Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 94557200B30 for ; Mon, 4 Jul 2016 14:38:20 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id 9303C160A55; Mon, 4 Jul 2016 12:38:20 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 32436160A76 for ; Mon, 4 Jul 2016 14:38:18 +0200 (CEST) Received: (qmail 4257 invoked by uid 500); 4 Jul 2016 12:38:16 -0000 Mailing-List: contact commits-help@cassandra.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@cassandra.apache.org Delivered-To: mailing list commits@cassandra.apache.org Received: (qmail 3942 invoked by uid 99); 4 Jul 2016 12:38:16 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 04 Jul 2016 12:38:16 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id A4EDDE03C0; Mon, 4 Jul 2016 12:38:16 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: blerer@apache.org To: commits@cassandra.apache.org Date: Mon, 04 Jul 2016 12:38:21 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [6/9] cassandra git commit: Merge branch cassandra-2.1 into cassandra-2.2 archived-at: Mon, 04 Jul 2016 12:38:20 -0000 Merge branch cassandra-2.1 into cassandra-2.2 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/e06dae81 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/e06dae81 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/e06dae81 Branch: refs/heads/cassandra-3.9 Commit: e06dae81fb08870ef6a6596b1557b88fc7762302 Parents: 65f8bb6 c857919 Author: Benjamin Lerer Authored: Mon Jul 4 14:20:34 2016 +0200 Committer: Benjamin Lerer Committed: Mon Jul 4 14:22:55 2016 +0200 ---------------------------------------------------------------------- CHANGES.txt | 1 + .../restrictions/MultiColumnRestriction.java | 2 +- .../restrictions/PrimaryKeyRestrictionSet.java | 81 ++++---------------- .../restrictions/StatementRestrictions.java | 42 ++++++++-- .../SelectMultiColumnRelationTest.java | 32 +++++++- .../cql3/validation/operations/SelectTest.java | 61 +++++++++++++++ 6 files changed, 142 insertions(+), 77 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/e06dae81/CHANGES.txt ---------------------------------------------------------------------- diff --cc CHANGES.txt index 13a1c4f,0967ce4..451575c --- a/CHANGES.txt +++ b/CHANGES.txt @@@ -1,41 -1,9 +1,42 @@@ -2.1.16 +2.2.8 + * MemoryUtil.getShort() should return an unsigned short also for architectures not supporting unaligned memory accesses (CASSANDRA-11973) +Merged from 2.1: + * Fix filtering on clustering columns when 2i is used (CASSANDRA-11907) - * Reduce contention getting instances of CompositeType (CASSANDRA-10433) - -2.1.15 * Account for partition deletions in tombstone histogram (CASSANDRA-12112) + + +2.2.7 + * Allow nodetool info to run with readonly JMX access (CASSANDRA-11755) + * Validate bloom_filter_fp_chance against lowest supported + value when the table is created (CASSANDRA-11920) + * RandomAccessReader: call isEOF() only when rebuffering, not for every read operation (CASSANDRA-12013) + * Don't send erroneous NEW_NODE notifications on restart (CASSANDRA-11038) + * StorageService shutdown hook should use a volatile variable (CASSANDRA-11984) + * Persist local metadata earlier in startup sequence (CASSANDRA-11742) + * Run CommitLog tests with different compression settings (CASSANDRA-9039) + * cqlsh: fix tab completion for case-sensitive identifiers (CASSANDRA-11664) + * Avoid showing estimated key as -1 in tablestats (CASSANDRA-11587) + * Fix possible race condition in CommitLog.recover (CASSANDRA-11743) + * Enable client encryption in sstableloader with cli options (CASSANDRA-11708) + * Possible memory leak in NIODataInputStream (CASSANDRA-11867) + * Fix commit log replay after out-of-order flush completion (CASSANDRA-9669) + * Add seconds to cqlsh tracing session duration (CASSANDRA-11753) + * Prohibit Reverse Counter type as part of the PK (CASSANDRA-9395) + * cqlsh: correctly handle non-ascii chars in error messages (CASSANDRA-11626) + * Exit JVM if JMX server fails to startup (CASSANDRA-11540) + * Produce a heap dump when exiting on OOM (CASSANDRA-9861) + * Avoid read repairing purgeable tombstones on range slices (CASSANDRA-11427) + * Restore ability to filter on clustering columns when using a 2i (CASSANDRA-11510) + * JSON datetime formatting needs timezone (CASSANDRA-11137) + * Fix is_dense recalculation for Thrift-updated tables (CASSANDRA-11502) + * Remove unnescessary file existence check during anticompaction (CASSANDRA-11660) + * Add missing files to debian packages (CASSANDRA-11642) + * Avoid calling Iterables::concat in loops during ModificationStatement::getFunctions (CASSANDRA-11621) + * cqlsh: COPY FROM should use regular inserts for single statement batches and + report errors correctly if workers processes crash on initialization (CASSANDRA-11474) + * Always close cluster with connection in CqlRecordWriter (CASSANDRA-11553) + * Fix slice queries on ordered COMPACT tables (CASSANDRA-10988) +Merged from 2.1: * Avoid stalling paxos when the paxos state expires (CASSANDRA-12043) * Remove finished incoming streaming connections from MessagingService (CASSANDRA-11854) * Don't try to get sstables for non-repairing column families (CASSANDRA-12077) http://git-wip-us.apache.org/repos/asf/cassandra/blob/e06dae81/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java ---------------------------------------------------------------------- diff --cc src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java index 96e6f2b,0000000..51e2ce4 mode 100644,000000..100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java @@@ -1,515 -1,0 +1,515 @@@ +/* + * 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.cql3.restrictions; + +import java.nio.ByteBuffer; +import java.util.*; + +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.Term.Terminal; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.db.IndexExpression; +import org.apache.cassandra.db.composites.CompositesBuilder; +import org.apache.cassandra.db.index.SecondaryIndex; +import org.apache.cassandra.db.index.SecondaryIndexManager; +import org.apache.cassandra.exceptions.InvalidRequestException; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +public abstract class MultiColumnRestriction extends AbstractRestriction +{ + /** + * The columns to which the restriction apply. + */ + protected final List columnDefs; + + public MultiColumnRestriction(List columnDefs) + { + this.columnDefs = columnDefs; + } + + @Override + public boolean isMultiColumn() + { + return true; + } + + @Override + public ColumnDefinition getFirstColumn() + { + return columnDefs.get(0); + } + + @Override + public ColumnDefinition getLastColumn() + { + return columnDefs.get(columnDefs.size() - 1); + } + + @Override + public List getColumnDefs() + { + return columnDefs; + } + + @Override + public final Restriction mergeWith(Restriction otherRestriction) throws InvalidRequestException + { + // We want to allow query like: (b,c) > (?, ?) AND b < ? + if (!otherRestriction.isMultiColumn() + && ((SingleColumnRestriction) otherRestriction).canBeConvertedToMultiColumnRestriction()) + { + return doMergeWith(((SingleColumnRestriction) otherRestriction).toMultiColumnRestriction()); + } + + return doMergeWith(otherRestriction); + } + + protected abstract Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException; + + /** + * Returns the names of the columns that are specified within this Restrictions and the other one + * as a comma separated String. + * + * @param otherRestriction the other restrictions + * @return the names of the columns that are specified within this Restrictions and the other one + * as a comma separated String. + */ + protected final String getColumnsInCommons(Restriction otherRestriction) + { + Set commons = new HashSet<>(getColumnDefs()); + commons.retainAll(otherRestriction.getColumnDefs()); + StringBuilder builder = new StringBuilder(); + for (ColumnDefinition columnDefinition : commons) + { + if (builder.length() != 0) + builder.append(" ,"); + builder.append(columnDefinition.name); + } + return builder.toString(); + } + + @Override + public final boolean hasSupportingIndex(SecondaryIndexManager indexManager) + { + for (ColumnDefinition columnDef : columnDefs) + { + SecondaryIndex index = indexManager.getIndexForColumn(columnDef.name.bytes); + if (index != null && isSupportedBy(index)) + return true; + } + return false; + } + + /** + * Check if this type of restriction is supported for by the specified index. + * @param index the Secondary index + * + * @return true this type of restriction is supported by the specified index, + * false otherwise. + */ + protected abstract boolean isSupportedBy(SecondaryIndex index); + + public static class EQ extends MultiColumnRestriction + { + protected final Term value; + + public EQ(List columnDefs, Term value) + { + super(columnDefs); + this.value = value; + } + + @Override + public Iterable getFunctions() + { + return value.getFunctions(); + } + + @Override + public String toString() + { + return String.format("EQ(%s)", value); + } + + @Override + public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException + { + throw invalidRequest("%s cannot be restricted by more than one relation if it includes an Equal", + getColumnsInCommons(otherRestriction)); + } + + @Override + protected boolean isSupportedBy(SecondaryIndex index) + { + return index.supportsOperator(Operator.EQ); + } + + @Override + public CompositesBuilder appendTo(CompositesBuilder builder, QueryOptions options) + { + Tuples.Value t = ((Tuples.Value) value.bind(options)); + List values = t.getElements(); + for (int i = 0, m = values.size(); i < m; i++) + { + builder.addElementToAll(values.get(i)); + checkFalse(builder.containsNull(), "Invalid null value for column %s", columnDefs.get(i).name); + } + return builder; + } + + @Override + public final void addIndexExpressionTo(List expressions, + SecondaryIndexManager indexManager, + QueryOptions options) throws InvalidRequestException + { + Tuples.Value t = ((Tuples.Value) value.bind(options)); + List values = t.getElements(); + + for (int i = 0, m = columnDefs.size(); i < m; i++) + { + ColumnDefinition columnDef = columnDefs.get(i); + ByteBuffer component = validateIndexedValue(columnDef, values.get(i)); + expressions.add(new IndexExpression(columnDef.name.bytes, Operator.EQ, component)); + } + } + } + + public abstract static class IN extends MultiColumnRestriction + { + /** + * {@inheritDoc} + */ + @Override + public CompositesBuilder appendTo(CompositesBuilder builder, QueryOptions options) + { + List> splitInValues = splitValues(options); + builder.addAllElementsToAll(splitInValues); + + if (builder.containsNull()) + throw invalidRequest("Invalid null value in condition for columns: %s", ColumnDefinition.toIdentifiers(columnDefs)); + return builder; + } + + public IN(List columnDefs) + { + super(columnDefs); + } + + @Override + public boolean isIN() + { + return true; + } + + @Override + public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException + { + throw invalidRequest("%s cannot be restricted by more than one relation if it includes a IN", + getColumnsInCommons(otherRestriction)); + } + + @Override + protected boolean isSupportedBy(SecondaryIndex index) + { + return index.supportsOperator(Operator.IN); + } + + @Override + public final void addIndexExpressionTo(List expressions, + SecondaryIndexManager indexManager, + QueryOptions options) throws InvalidRequestException + { + List> splitInValues = splitValues(options); + checkTrue(splitInValues.size() == 1, "IN restrictions are not supported on indexed columns"); + List values = splitInValues.get(0); + + for (int i = 0, m = columnDefs.size(); i < m; i++) + { + ColumnDefinition columnDef = columnDefs.get(i); + ByteBuffer component = validateIndexedValue(columnDef, values.get(i)); + expressions.add(new IndexExpression(columnDef.name.bytes, Operator.EQ, component)); + } + } + + protected abstract List> splitValues(QueryOptions options) throws InvalidRequestException; + } + + /** + * An IN restriction that has a set of terms for in values. + * For example: "SELECT ... WHERE (a, b, c) IN ((1, 2, 3), (4, 5, 6))" or "WHERE (a, b, c) IN (?, ?)" + */ + public static class InWithValues extends MultiColumnRestriction.IN + { + protected final List values; + + public InWithValues(List columnDefs, List values) + { + super(columnDefs); + this.values = values; + } + + @Override + public Iterable getFunctions() + { + return Terms.getFunctions(values); + } + + @Override + public String toString() + { + return String.format("IN(%s)", values); + } + + @Override + protected List> splitValues(QueryOptions options) throws InvalidRequestException + { + List> buffers = new ArrayList<>(values.size()); + for (Term value : values) + { + Term.MultiItemTerminal term = (Term.MultiItemTerminal) value.bind(options); + buffers.add(term.getElements()); + } + return buffers; + } + } + + /** + * An IN restriction that uses a single marker for a set of IN values that are tuples. + * For example: "SELECT ... WHERE (a, b, c) IN ?" + */ + public static class InWithMarker extends MultiColumnRestriction.IN + { + protected final AbstractMarker marker; + + public InWithMarker(List columnDefs, AbstractMarker marker) + { + super(columnDefs); + this.marker = marker; + } + + @Override + public Iterable getFunctions() + { + return Collections.emptySet(); + } + + @Override + public String toString() + { + return "IN ?"; + } + + @Override + protected List> splitValues(QueryOptions options) throws InvalidRequestException + { + Tuples.InMarker inMarker = (Tuples.InMarker) marker; + Tuples.InValue inValue = inMarker.bind(options); + checkNotNull(inValue, "Invalid null value for IN restriction"); + return inValue.getSplitValues(); + } + } + + public static class Slice extends MultiColumnRestriction + { + private final TermSlice slice; + + public Slice(List columnDefs, Bound bound, boolean inclusive, Term term) + { + this(columnDefs, TermSlice.newInstance(bound, inclusive, term)); + } + + Slice(List columnDefs, TermSlice slice) + { + super(columnDefs); + this.slice = slice; + } + + @Override + public boolean isSlice() + { + return true; + } + + @Override + public CompositesBuilder appendTo(CompositesBuilder builder, QueryOptions options) + { + throw new UnsupportedOperationException(); + } + + @Override + public CompositesBuilder appendBoundTo(CompositesBuilder builder, Bound bound, QueryOptions options) + { + boolean reversed = getFirstColumn().isReversedType(); + + EnumMap> componentBounds = new EnumMap>(Bound.class); + componentBounds.put(Bound.START, componentBounds(Bound.START, options)); + componentBounds.put(Bound.END, componentBounds(Bound.END, options)); + + List> toAdd = new ArrayList<>(); + List values = new ArrayList<>(); + + for (int i = 0, m = columnDefs.size(); i < m; i++) + { + ColumnDefinition column = columnDefs.get(i); + Bound b = reverseBoundIfNeeded(column, bound); + + // For mixed order columns, we need to create additional slices when 2 columns are in reverse order + if (reversed != column.isReversedType()) + { + reversed = column.isReversedType(); + // As we are switching direction we need to add the current composite + toAdd.add(values); + + // The new bound side has no value for this component. just stop + if (!hasComponent(b, i, componentBounds)) + continue; + + // The other side has still some components. We need to end the slice that we have just open. + if (hasComponent(b.reverse(), i, componentBounds)) + toAdd.add(values); + + // We need to rebuild where we are in this bound side + values = new ArrayList(); + + List vals = componentBounds.get(b); + + int n = Math.min(i, vals.size()); + for (int j = 0; j < n; j++) + { + ByteBuffer v = checkNotNull(vals.get(j), + "Invalid null value in condition for column %s", + columnDefs.get(j).name); + values.add(v); + } + } + + if (!hasComponent(b, i, componentBounds)) + continue; + + ByteBuffer v = checkNotNull(componentBounds.get(b).get(i), "Invalid null value in condition for column %s", columnDefs.get(i).name); + values.add(v); + } + toAdd.add(values); + + if (bound.isEnd()) + Collections.reverse(toAdd); + + return builder.addAllElementsToAll(toAdd); + } + + @Override + protected boolean isSupportedBy(SecondaryIndex index) + { + return slice.isSupportedBy(index); + } + + @Override + public boolean hasBound(Bound bound) + { + return slice.hasBound(bound); + } + + @Override + public Iterable getFunctions() + { + return slice.getFunctions(); + } + + @Override + public boolean isInclusive(Bound bound) + { + return slice.isInclusive(bound); + } + + @Override + public Restriction doMergeWith(Restriction otherRestriction) throws InvalidRequestException + { + checkTrue(otherRestriction.isSlice(), + "Column \"%s\" cannot be restricted by both an equality and an inequality relation", + getColumnsInCommons(otherRestriction)); + + if (!getFirstColumn().equals(otherRestriction.getFirstColumn())) + { + ColumnDefinition column = getFirstColumn().position() > otherRestriction.getFirstColumn().position() + ? getFirstColumn() : otherRestriction.getFirstColumn(); + + throw invalidRequest("Column \"%s\" cannot be restricted by two inequalities not starting with the same column", + column.name); + } + + checkFalse(hasBound(Bound.START) && otherRestriction.hasBound(Bound.START), + "More than one restriction was found for the start bound on %s", + getColumnsInCommons(otherRestriction)); + checkFalse(hasBound(Bound.END) && otherRestriction.hasBound(Bound.END), + "More than one restriction was found for the end bound on %s", + getColumnsInCommons(otherRestriction)); + + Slice otherSlice = (Slice) otherRestriction; + List newColumnDefs = columnDefs.size() >= otherSlice.columnDefs.size() ? columnDefs : otherSlice.columnDefs; + + return new Slice(newColumnDefs, slice.merge(otherSlice.slice)); + } + + @Override + public final void addIndexExpressionTo(List expressions, + SecondaryIndexManager indexManager, + QueryOptions options) throws InvalidRequestException + { - throw invalidRequest("Slice restrictions are not supported on indexed columns"); ++ throw invalidRequest("Multi-column slice restrictions cannot be used for filtering."); + } + + @Override + public String toString() + { + return "SLICE" + slice; + } + + /** + * Similar to bounds(), but returns one ByteBuffer per-component in the bound instead of a single + * ByteBuffer to represent the entire bound. + * @param b the bound type + * @param options the query options + * @return one ByteBuffer per-component in the bound + * @throws InvalidRequestException if the components cannot be retrieved + */ + private List componentBounds(Bound b, QueryOptions options) throws InvalidRequestException + { + if (!slice.hasBound(b)) + return Collections.emptyList(); + + Terminal terminal = slice.bound(b).bind(options); + + if (terminal instanceof Tuples.Value) + { + return ((Tuples.Value) terminal).getElements(); + } + + return Collections.singletonList(terminal.get(options.getProtocolVersion())); + } + + private boolean hasComponent(Bound b, int index, EnumMap> componentBounds) + { + return componentBounds.get(b).size() > index; + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/e06dae81/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java ---------------------------------------------------------------------- diff --cc src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java index 0c10f13,0000000..e1cbc29 mode 100644,000000..100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/PrimaryKeyRestrictionSet.java @@@ -1,460 -1,0 +1,405 @@@ +/* + * 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.cql3.restrictions; + +import java.nio.ByteBuffer; +import java.util.*; + +import com.google.common.collect.Lists; + - import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.db.IndexExpression; - import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.composites.*; +import org.apache.cassandra.db.composites.Composite.EOC; +import org.apache.cassandra.db.index.SecondaryIndexManager; +import org.apache.cassandra.exceptions.InvalidRequestException; + - import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; - import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; - +/** + * A set of single column restrictions on a primary key part (partition key or clustering key). + */ - final class PrimaryKeyRestrictionSet extends AbstractPrimaryKeyRestrictions ++final class PrimaryKeyRestrictionSet extends AbstractPrimaryKeyRestrictions implements Iterable +{ + /** + * The restrictions. + */ + private final RestrictionSet restrictions; + + /** + * true if the restrictions are corresponding to an EQ, false otherwise. + */ + private boolean eq; + + /** + * true if the restrictions are corresponding to an IN, false otherwise. + */ + private boolean in; + + /** + * true if the restrictions are corresponding to a Slice, false otherwise. + */ + private boolean slice; + + /** + * true if the restrictions are corresponding to a Contains, false otherwise. + */ + private boolean contains; + - - /** - * If restrictions apply to clustering columns, we need to check whether they can be satisfied by an index lookup - * as this affects which other restrictions can legally be specified (if an index is present, we are more lenient - * about what additional filtering can be performed on the results of a lookup - see CASSANDRA-11510). - * - * We don't hold a reference to the SecondaryIndexManager itself as this is not strictly a singleton (although - * we often treat is as one), the field would also require annotation with @Unmetered to avoid blowing up the - * object size (used when calculating the size of prepared statements for caching). Instead, we refer to the - * CFMetaData and retrieve the index manager when necessary. - * - * There are a couple of scenarios where the CFM can be null (and we make sure and test for null when we use it): - * * where an empty set of restrictions are created for use in processing query results - see - * SelectStatement.forSelection - * * where the restrictions apply to partition keys and not clustering columns e.g. - * StatementRestrictions.partitionKeyRestrictions - * * in unit tests (in particular PrimaryKeyRestrictionSetTest which is primarily concerned with the correct - * generation of bounds when secondary indexes are not used). - */ - private final CFMetaData cfm; - + public PrimaryKeyRestrictionSet(CType ctype) + { - this(ctype, null); - } - - public PrimaryKeyRestrictionSet(CType ctype, CFMetaData cfm) - { + super(ctype); - this.cfm = cfm; + this.restrictions = new RestrictionSet(); + this.eq = true; + } + + private PrimaryKeyRestrictionSet(PrimaryKeyRestrictionSet primaryKeyRestrictions, + Restriction restriction) throws InvalidRequestException + { + super(primaryKeyRestrictions.ctype); + this.restrictions = primaryKeyRestrictions.restrictions.addRestriction(restriction); - this.cfm = primaryKeyRestrictions.cfm; - - if (!primaryKeyRestrictions.isEmpty() && !hasSupportingIndex(restriction)) - { - ColumnDefinition lastRestrictionStart = primaryKeyRestrictions.restrictions.lastRestriction().getFirstColumn(); - ColumnDefinition newRestrictionStart = restriction.getFirstColumn(); - - checkFalse(primaryKeyRestrictions.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position(), - "Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", - newRestrictionStart.name, - lastRestrictionStart.name); - - if (newRestrictionStart.position() < lastRestrictionStart.position() && restriction.isSlice()) - throw invalidRequest("PRIMARY KEY column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", - restrictions.nextColumn(newRestrictionStart).name, - newRestrictionStart.name); - } + + if (restriction.isSlice() || primaryKeyRestrictions.isSlice()) + this.slice = true; + else if (restriction.isContains() || primaryKeyRestrictions.isContains()) + this.contains = true; + else if (restriction.isIN() || primaryKeyRestrictions.isIN()) + this.in = true; + else + this.eq = true; + } + - private boolean hasSupportingIndex(Restriction restriction) - { - return cfm != null - && restriction.hasSupportingIndex(Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfId).indexManager); - } - + @Override + public boolean isSlice() + { + return slice; + } + + @Override + public boolean isEQ() + { + return eq; + } + + @Override + public boolean isIN() + { + return in; + } + + @Override + public boolean isOnToken() + { + return false; + } + + @Override + public boolean isContains() + { + return contains; + } + + @Override + public boolean isMultiColumn() + { + return false; + } + + @Override + public Iterable getFunctions() + { + return restrictions.getFunctions(); + } + + @Override + public PrimaryKeyRestrictions mergeWith(Restriction restriction) throws InvalidRequestException + { + if (restriction.isOnToken()) + { + if (isEmpty()) + return (PrimaryKeyRestrictions) restriction; + + return new TokenFilter(this, (TokenRestriction) restriction); + } + + return new PrimaryKeyRestrictionSet(this, restriction); + } + + @Override + public List valuesAsComposites(QueryOptions options) throws InvalidRequestException + { + return filterAndSort(appendTo(new CompositesBuilder(ctype), options).build()); + } + + @Override + public CompositesBuilder appendTo(CompositesBuilder builder, QueryOptions options) + { + for (Restriction r : restrictions) + { + r.appendTo(builder, options); + if (builder.hasMissingElements()) + break; + } + return builder; + } + + @Override + public CompositesBuilder appendBoundTo(CompositesBuilder builder, Bound bound, QueryOptions options) + { + throw new UnsupportedOperationException(); + } + + @Override + public List boundsAsComposites(Bound bound, QueryOptions options) throws InvalidRequestException + { + CompositesBuilder builder = new CompositesBuilder(ctype); + // 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), + // it only depends on whether the slice is reversed + int keyPosition = 0; + for (Restriction r : restrictions) + { + ColumnDefinition def = r.getFirstColumn(); + + if (keyPosition != def.position() || r.isContains()) + break; + + if (r.isSlice()) + { + r.appendBoundTo(builder, bound, options); + + // Since CASSANDRA-7281, the composites might not end with the same components and it is possible + // that one of the composites is an empty one. Unfortunatly, AbstractCType will always sort + // Composites.EMPTY before all the other components due to its EOC, even if it is not the desired + // behaviour in some cases. To avoid that problem the code will use normal composites for the empty + // ones until the composites are properly sorted. They will then be replaced by Composites.EMPTY as + // it is what is expected by the intra-node serialization. + // It is clearly a hack but it does not make a lot of sense to refactor 2.2 for that as the problem is + // already solved in 3.0. + List composites = filterAndSort(setEocs(r, bound, builder.build())); + return Lists.transform(composites, new com.google.common.base.Function() + { + @Override + public Composite apply(Composite composite) + { + return composite.isEmpty() ? Composites.EMPTY: composite; + } + }); + } + + r.appendBoundTo(builder, bound, options); + + if (builder.hasMissingElements()) + return Collections.emptyList(); + + keyPosition = r.getLastColumn().position() + 1; + } + // 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, + // it would be harmless to do it. However, we use this method got the partition key too. And when a query + // 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). + EOC eoc = !builder.hasRemaining() ? EOC.NONE : (bound.isEnd() ? EOC.END : EOC.START); + return filterAndSort(builder.buildWithEOC(eoc)); + } + + /** + * Removes duplicates and sort the specified composites. + * + * @param composites the composites to filter and sort + * @return the composites sorted and without duplicates + */ + private List filterAndSort(List composites) + { + if (composites.size() <= 1) + return composites; + + TreeSet set = new TreeSet(ctype); + set.addAll(composites); + + return new ArrayList<>(set); + } + + /** + * Sets EOCs for the composites returned by the specified slice restriction for the given bound. + * + * @param r the slice restriction + * @param bound the bound + * @param composites the composites + * @return the composites with their EOCs properly set + */ + private List setEocs(Restriction r, Bound bound, List composites) + { + List list = new ArrayList<>(composites.size()); + + // The first column of the slice might not be the first clustering column (e.g. clustering_0 = ? AND (clustering_1, clustering_2) >= (?, ?) + int offset = r.getFirstColumn().position(); + + for (int i = 0, m = composites.size(); i < m; i++) + { + Composite composite = composites.get(i); + + // Handle the no bound case + if (composite.size() == offset) + { + list.add(composite.withEOC(bound.isEnd() ? EOC.END : EOC.START)); + continue; + } + + // In the case of mixed order columns, we will have some extra slices where the columns change directions. + // For example: if we have clustering_0 DESC and clustering_1 ASC a slice like (clustering_0, clustering_1) > (1, 2) + // will produce 2 slices: [EMPTY, 1.START] and [1.2.END, 1.END] + // So, the END bound will return 2 composite with the same values 1 + if (composite.size() <= r.getLastColumn().position() && i < m - 1 && composite.equals(composites.get(i + 1))) + { + list.add(composite.withEOC(EOC.START)); + list.add(composites.get(i++).withEOC(EOC.END)); + continue; + } + + // Handle the normal bounds + ColumnDefinition column = r.getColumnDefs().get(composite.size() - 1 - offset); + Bound b = reverseBoundIfNeeded(column, bound); + + Composite.EOC eoc = eocFor(r, bound, b); + list.add(composite.withEOC(eoc)); + } + + return list; + } + + @Override + public List values(QueryOptions options) throws InvalidRequestException + { + return Composites.toByteBuffers(valuesAsComposites(options)); + } + + @Override + public List bounds(Bound b, QueryOptions options) throws InvalidRequestException + { + return Composites.toByteBuffers(boundsAsComposites(b, options)); + } + + private static Composite.EOC eocFor(Restriction r, Bound eocBound, Bound inclusiveBound) + { + if (eocBound.isStart()) + return r.isInclusive(inclusiveBound) ? Composite.EOC.NONE : Composite.EOC.END; + + return r.isInclusive(inclusiveBound) ? Composite.EOC.END : Composite.EOC.START; + } + + @Override + public boolean hasBound(Bound b) + { + if (isEmpty()) + return false; + return restrictions.lastRestriction().hasBound(b); + } + + @Override + public boolean isInclusive(Bound b) + { + if (isEmpty()) + return false; + return restrictions.lastRestriction().isInclusive(b); + } + + @Override + public boolean hasSupportingIndex(SecondaryIndexManager indexManager) + { + return restrictions.hasSupportingIndex(indexManager); + } + + @Override + public void addIndexExpressionTo(List expressions, + SecondaryIndexManager indexManager, + QueryOptions options) throws InvalidRequestException + { + Boolean clusteringColumns = null; + int position = 0; + + for (Restriction restriction : restrictions) + { + ColumnDefinition columnDef = restriction.getFirstColumn(); + + // PrimaryKeyRestrictionSet contains only one kind of column, either partition key or clustering columns. + // Therefore we only need to check the column kind once. All the other columns will be of the same kind. + if (clusteringColumns == null) + clusteringColumns = columnDef.isClusteringColumn() ? Boolean.TRUE : Boolean.FALSE; + + // We ignore all the clustering columns that can be handled by slices. - if (clusteringColumns && !restriction.isContains()&& position == columnDef.position()) ++ if (!clusteringColumns || handleInFilter(restriction, position) || restriction.hasSupportingIndex(indexManager)) + { - position = restriction.getLastColumn().position() + 1; - if (!restriction.hasSupportingIndex(indexManager)) - continue; ++ restriction.addIndexExpressionTo(expressions, indexManager, options); ++ continue; + } - restriction.addIndexExpressionTo(expressions, indexManager, options); ++ ++ if (!restriction.isSlice()) ++ position = restriction.getLastColumn().position() + 1; + } + } + + @Override + public List getColumnDefs() + { + return restrictions.getColumnDefs(); + } + + @Override + public ColumnDefinition getFirstColumn() + { + return restrictions.firstColumn(); + } + + @Override + public ColumnDefinition getLastColumn() + { + return restrictions.lastColumn(); + } + + public final boolean needsFiltering() + { + // Backported from ClusteringColumnRestrictions from CASSANDRA-11310 for 3.6 + // As that suggests, this should only be called on clustering column + // and not partition key restrictions. + int position = 0; - Restriction slice = null; + for (Restriction restriction : restrictions) + { + if (handleInFilter(restriction, position)) + return true; + - if (slice != null && !slice.getFirstColumn().equals(restriction.getFirstColumn())) - return true; - - if (slice == null && restriction.isSlice()) - slice = restriction; - else ++ if (!restriction.isSlice()) + position = restriction.getLastColumn().position() + 1; + } + + return false; + } + + private boolean handleInFilter(Restriction restriction, int index) + { + return restriction.isContains() || index != restriction.getFirstColumn().position(); + } ++ ++ public Iterator iterator() ++ { ++ return restrictions.iterator(); ++ } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/e06dae81/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java ---------------------------------------------------------------------- diff --cc src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java index 6f03c40,0000000..1547210 mode 100644,000000..100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java @@@ -1,613 -1,0 +1,645 @@@ +/* + * 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.cql3.restrictions; + +import java.nio.ByteBuffer; +import java.util.*; + +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; + +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.config.ColumnDefinition; - import org.apache.cassandra.config.Schema; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.db.*; +import org.apache.cassandra.db.composites.Composite; +import org.apache.cassandra.db.index.SecondaryIndexManager; +import org.apache.cassandra.dht.*; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.ByteBufferUtil; + +import static org.apache.cassandra.config.ColumnDefinition.toIdentifiers; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; - import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +/** + * The restrictions corresponding to the relations specified on the where-clause of CQL query. + */ +public final class StatementRestrictions +{ + public static final String REQUIRES_ALLOW_FILTERING_MESSAGE = + "Cannot execute this query as it might involve data filtering and " + + "thus may have unpredictable performance. If you want to execute " + + "this query despite the performance unpredictability, use ALLOW FILTERING"; + + /** + * The Column Family meta data + */ + public final CFMetaData cfm; + + /** + * Restrictions on partitioning columns + */ + private PrimaryKeyRestrictions partitionKeyRestrictions; + + /** + * Restrictions on clustering columns + */ + private PrimaryKeyRestrictions clusteringColumnsRestrictions; + + /** + * Restriction on non-primary key columns (i.e. secondary index restrictions) + */ + private RestrictionSet nonPrimaryKeyRestrictions; + + /** + * The restrictions used to build the index expressions + */ + private final List indexRestrictions = new ArrayList<>(); + + /** + * true if the secondary index need to be queried, false otherwise + */ + private boolean usesSecondaryIndexing; + + /** + * Specify if the query will return a range of partition keys. + */ + private boolean isKeyRange; + + /** + * Creates a new empty StatementRestrictions. + * + * @param cfm the column family meta data + * @return a new empty StatementRestrictions. + */ + public static StatementRestrictions empty(CFMetaData cfm) + { + return new StatementRestrictions(cfm); + } + + private StatementRestrictions(CFMetaData cfm) + { + this.cfm = cfm; + this.partitionKeyRestrictions = new PrimaryKeyRestrictionSet(cfm.getKeyValidatorAsCType()); + this.clusteringColumnsRestrictions = new PrimaryKeyRestrictionSet(cfm.comparator); + this.nonPrimaryKeyRestrictions = new RestrictionSet(); + } + + public StatementRestrictions(CFMetaData cfm, + List whereClause, + VariableSpecifications boundNames, + boolean selectsOnlyStaticColumns, + boolean selectACollection, - boolean useFiltering) ++ boolean allowFiltering) + { + this.cfm = cfm; + this.partitionKeyRestrictions = new PrimaryKeyRestrictionSet(cfm.getKeyValidatorAsCType()); - this.clusteringColumnsRestrictions = new PrimaryKeyRestrictionSet(cfm.comparator, cfm); ++ this.clusteringColumnsRestrictions = new PrimaryKeyRestrictionSet(cfm.comparator); + this.nonPrimaryKeyRestrictions = new RestrictionSet(); + + /* + * WHERE clause. For a given entity, rules are: - EQ relation conflicts with anything else (including a 2nd EQ) + * - Can't have more than one LT(E) relation (resp. GT(E) relation) - IN relation are restricted to row keys + * (for now) and conflicts with anything else (we could allow two IN for the same entity but that doesn't seem + * very useful) - The value_alias cannot be restricted in any way (we don't support wide rows with indexed value + * in CQL so far) + */ + for (Relation relation : whereClause) + addRestriction(relation.toRestriction(cfm, boundNames)); + + SecondaryIndexManager secondaryIndexManager = Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfName).indexManager; + boolean hasQueriableClusteringColumnIndex = clusteringColumnsRestrictions.hasSupportingIndex(secondaryIndexManager); + boolean hasQueriableIndex = hasQueriableClusteringColumnIndex + || partitionKeyRestrictions.hasSupportingIndex(secondaryIndexManager) + || nonPrimaryKeyRestrictions.hasSupportingIndex(secondaryIndexManager); + + // At this point, the select statement if fully constructed, but we still have a few things to validate + processPartitionKeyRestrictions(hasQueriableIndex); + + // Some but not all of the partition key columns have been specified; + // hence we need turn these restrictions into index expressions. + if (usesSecondaryIndexing) + indexRestrictions.add(partitionKeyRestrictions); + + checkFalse(selectsOnlyStaticColumns && hasClusteringColumnsRestriction(), + "Cannot restrict clustering columns when selecting only static columns"); + + processClusteringColumnsRestrictions(hasQueriableIndex, selectACollection); + + // Covers indexes on the first clustering column (among others). + if (isKeyRange && hasQueriableClusteringColumnIndex) + usesSecondaryIndexing = true; + + usesSecondaryIndexing = usesSecondaryIndexing || clusteringColumnsRestrictions.isContains(); + + if (usesSecondaryIndexing) + indexRestrictions.add(clusteringColumnsRestrictions); + + // Even if usesSecondaryIndexing is false at this point, we'll still have to use one if + // there is restrictions not covered by the PK. + if (!nonPrimaryKeyRestrictions.isEmpty()) + { + if (!hasQueriableIndex) + { + // Filtering for non-index query is only supported for thrift static CFs + if (cfm.comparator.isDense() || cfm.comparator.isCompound()) + throw invalidRequest("Predicates on non-primary-key columns (%s) are not yet supported for non secondary index queries", + Joiner.on(", ").join(toIdentifiers(nonPrimaryKeyRestrictions.getColumnDefs()))); + - if (!useFiltering) ++ if (!allowFiltering) + throw invalidRequest(REQUIRES_ALLOW_FILTERING_MESSAGE); + } + usesSecondaryIndexing = true; + indexRestrictions.add(nonPrimaryKeyRestrictions); + } + + if (usesSecondaryIndexing) + validateSecondaryIndexSelections(selectsOnlyStaticColumns); + } + + private void addRestriction(Restriction restriction) throws InvalidRequestException + { + if (restriction.isMultiColumn()) + clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction); + else if (restriction.isOnToken()) + partitionKeyRestrictions = partitionKeyRestrictions.mergeWith(restriction); + else + addSingleColumnRestriction((SingleColumnRestriction) restriction); + } + + public Iterable getFunctions() + { + return Iterables.concat(partitionKeyRestrictions.getFunctions(), + clusteringColumnsRestrictions.getFunctions(), + nonPrimaryKeyRestrictions.getFunctions()); + } + + private void addSingleColumnRestriction(SingleColumnRestriction restriction) throws InvalidRequestException + { + ColumnDefinition def = restriction.columnDef; + if (def.isPartitionKey()) + partitionKeyRestrictions = partitionKeyRestrictions.mergeWith(restriction); + else if (def.isClusteringColumn()) + clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction); + else + nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictions.addRestriction(restriction); + } + + /** + * Checks if the restrictions on the partition key is an IN restriction. + * + * @return true the restrictions on the partition key is an IN restriction, false + * otherwise. + */ + public boolean keyIsInRelation() + { + return partitionKeyRestrictions.isIN(); + } + + /** + * Checks if the query request a range of partition keys. + * + * @return true if the query request a range of partition keys, false otherwise. + */ + public boolean isKeyRange() + { + return this.isKeyRange; + } + + /** + * Checks if the secondary index need to be queried. + * + * @return true if the secondary index need to be queried, false otherwise. + */ + public boolean usesSecondaryIndexing() + { + return this.usesSecondaryIndexing; + } + + private void processPartitionKeyRestrictions(boolean hasQueriableIndex) throws InvalidRequestException + { + // If there is a queriable index, no special condition are required on the other restrictions. + // But we still need to know 2 things: + // - If we don't have a queriable index, is the query ok + // - Is it queriable without 2ndary index, which is always more efficient + // If a component of the partition key is restricted by a relation, all preceding + // components must have a EQ. Only the last partition key component can be in IN relation. + if (partitionKeyRestrictions.isOnToken()) + isKeyRange = true; + + if (hasPartitionKeyUnrestrictedComponents()) + { + if (!partitionKeyRestrictions.isEmpty()) + { + if (!hasQueriableIndex) + throw invalidRequest("Partition key parts: %s must be restricted as other parts are", + Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents())); + } + + isKeyRange = true; + usesSecondaryIndexing = hasQueriableIndex; + } + } + + /** + * Checks if the partition key has some unrestricted components. + * @return true if the partition key has some unrestricted components, false otherwise. + */ + private boolean hasPartitionKeyUnrestrictedComponents() + { + return partitionKeyRestrictions.size() < cfm.partitionKeyColumns().size(); + } + + public boolean hasPartitionKeyRestrictions() + { + return !partitionKeyRestrictions.isEmpty(); + } + + /** + * Checks if the restrictions contain any non-primary key restrictions + * @return true if the restrictions contain any non-primary key restrictions, false otherwise. + */ + public boolean hasNonPrimaryKeyRestrictions() + { + return !nonPrimaryKeyRestrictions.isEmpty(); + } + + /** + * Returns the partition key components that are not restricted. + * @return the partition key components that are not restricted. + */ + private List getPartitionKeyUnrestrictedComponents() + { + List list = new ArrayList<>(cfm.partitionKeyColumns()); + list.removeAll(partitionKeyRestrictions.getColumnDefs()); + return ColumnDefinition.toIdentifiers(list); + } + + /** + * Processes the clustering column restrictions. + * + * @param hasQueriableIndex true if some of the queried data are indexed, false otherwise + * @param selectACollection true if the query should return a collection column + * @throws InvalidRequestException if the request is invalid + */ + private void processClusteringColumnsRestrictions(boolean hasQueriableIndex, + boolean selectACollection) throws InvalidRequestException + { ++ validateClusteringRestrictions(hasQueriableIndex); ++ + checkFalse(clusteringColumnsRestrictions.isIN() && selectACollection, + "Cannot restrict clustering columns by IN relations when a collection is selected by the query"); + checkFalse(clusteringColumnsRestrictions.isContains() && !hasQueriableIndex, + "Cannot restrict clustering columns by a CONTAINS relation without a secondary index"); + + if (hasClusteringColumnsRestriction() && clusteringRestrictionsNeedFiltering()) + { + if (hasQueriableIndex) + { + usesSecondaryIndexing = true; + return; + } + + List clusteringColumns = cfm.clusteringColumns(); + List restrictedColumns = new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs()); + + for (int i = 0, m = restrictedColumns.size(); i < m; i++) + { + ColumnDefinition clusteringColumn = clusteringColumns.get(i); + ColumnDefinition restrictedColumn = restrictedColumns.get(i); + + if (!clusteringColumn.equals(restrictedColumn)) + { + throw invalidRequest( + "PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted", + restrictedColumn.name, + clusteringColumn.name); + } + } + } + } + ++ /** ++ * Validates whether or not restrictions are allowed for execution when secondary index is not used. ++ */ ++ public final void validateClusteringRestrictions(boolean hasQueriableIndex) ++ { ++ assert clusteringColumnsRestrictions instanceof PrimaryKeyRestrictionSet; ++ ++ // If there's a queriable index, filtering will take care of clustering restrictions ++ if (hasQueriableIndex) ++ return; ++ ++ Iterator iter = ((PrimaryKeyRestrictionSet)clusteringColumnsRestrictions).iterator(); ++ Restriction previousRestriction = null; ++ ++ while (iter.hasNext()) ++ { ++ Restriction restriction = iter.next(); ++ ++ if (previousRestriction != null) ++ { ++ ColumnDefinition lastRestrictionStart = previousRestriction.getFirstColumn(); ++ ColumnDefinition newRestrictionStart = restriction.getFirstColumn(); ++ ++ if (previousRestriction.isSlice() && newRestrictionStart.position() > lastRestrictionStart.position()) ++ throw invalidRequest("Clustering column \"%s\" cannot be restricted (preceding column \"%s\" is restricted by a non-EQ relation)", ++ newRestrictionStart.name, ++ lastRestrictionStart.name); ++ } ++ previousRestriction = restriction; ++ } ++ } ++ + public final boolean clusteringRestrictionsNeedFiltering() + { + assert clusteringColumnsRestrictions instanceof PrimaryKeyRestrictionSet; + return ((PrimaryKeyRestrictionSet) clusteringColumnsRestrictions).needsFiltering(); + } + + public List getIndexExpressions(SecondaryIndexManager indexManager, + QueryOptions options) throws InvalidRequestException + { + if (!usesSecondaryIndexing || indexRestrictions.isEmpty()) + return Collections.emptyList(); + + List expressions = new ArrayList<>(); + for (Restrictions restrictions : indexRestrictions) + restrictions.addIndexExpressionTo(expressions, indexManager, options); + + return expressions; + } + + /** + * Returns the partition keys for which the data is requested. + * + * @param options the query options + * @return the partition keys for which the data is requested. + * @throws InvalidRequestException if the partition keys cannot be retrieved + */ + public Collection getPartitionKeys(final QueryOptions options) throws InvalidRequestException + { + return partitionKeyRestrictions.values(options); + } + + /** + * Returns the specified bound of the partition key. + * + * @param b the boundary type + * @param options the query options + * @return the specified bound of the partition key + * @throws InvalidRequestException if the boundary cannot be retrieved + */ + private ByteBuffer getPartitionKeyBound(Bound b, QueryOptions options) throws InvalidRequestException + { + // Deal with unrestricted partition key components (special-casing is required to deal with 2i queries on the + // first + // component of a composite partition key). + if (hasPartitionKeyUnrestrictedComponents()) + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + + // We deal with IN queries for keys in other places, so we know buildBound will return only one result + return partitionKeyRestrictions.bounds(b, options).get(0); + } + + /** + * Returns the partition key bounds. + * + * @param options the query options + * @return the partition key bounds + * @throws InvalidRequestException if the query is invalid + */ + public AbstractBounds getPartitionKeyBounds(QueryOptions options) throws InvalidRequestException + { + IPartitioner p = StorageService.getPartitioner(); + + if (partitionKeyRestrictions.isOnToken()) + { + return getPartitionKeyBoundsForTokenRestrictions(p, options); + } + + return getPartitionKeyBounds(p, options); + } + + private AbstractBounds getPartitionKeyBounds(IPartitioner p, + QueryOptions options) throws InvalidRequestException + { + ByteBuffer startKeyBytes = getPartitionKeyBound(Bound.START, options); + ByteBuffer finishKeyBytes = getPartitionKeyBound(Bound.END, options); + + RowPosition startKey = RowPosition.ForKey.get(startKeyBytes, p); + RowPosition finishKey = RowPosition.ForKey.get(finishKeyBytes, p); + + if (startKey.compareTo(finishKey) > 0 && !finishKey.isMinimum()) + return null; + + if (partitionKeyRestrictions.isInclusive(Bound.START)) + { + return partitionKeyRestrictions.isInclusive(Bound.END) + ? new Bounds<>(startKey, finishKey) + : new IncludingExcludingBounds<>(startKey, finishKey); + } + + return partitionKeyRestrictions.isInclusive(Bound.END) + ? new Range<>(startKey, finishKey) + : new ExcludingBounds<>(startKey, finishKey); + } + + private AbstractBounds getPartitionKeyBoundsForTokenRestrictions(IPartitioner p, + QueryOptions options) + throws InvalidRequestException + { + Token startToken = getTokenBound(Bound.START, options, p); + Token endToken = getTokenBound(Bound.END, options, p); + + boolean includeStart = partitionKeyRestrictions.isInclusive(Bound.START); + boolean includeEnd = partitionKeyRestrictions.isInclusive(Bound.END); + + /* + * If we ask SP.getRangeSlice() for (token(200), token(200)], it will happily return the whole ring. + * However, wrapping range doesn't really make sense for CQL, and we want to return an empty result in that + * case (CASSANDRA-5573). So special case to create a range that is guaranteed to be empty. + * + * In practice, we want to return an empty result set if either startToken > endToken, or both are equal but + * one of the bound is excluded (since [a, a] can contains something, but not (a, a], [a, a) or (a, a)). + * Note though that in the case where startToken or endToken is the minimum token, then this special case + * rule should not apply. + */ + int cmp = startToken.compareTo(endToken); + if (!startToken.isMinimum() && !endToken.isMinimum() + && (cmp > 0 || (cmp == 0 && (!includeStart || !includeEnd)))) + return null; + + RowPosition start = includeStart ? startToken.minKeyBound() : startToken.maxKeyBound(); + RowPosition end = includeEnd ? endToken.maxKeyBound() : endToken.minKeyBound(); + + return new Range<>(start, end); + } + + private Token getTokenBound(Bound b, QueryOptions options, IPartitioner p) throws InvalidRequestException + { + if (!partitionKeyRestrictions.hasBound(b)) + return p.getMinimumToken(); + + ByteBuffer value = partitionKeyRestrictions.bounds(b, options).get(0); + checkNotNull(value, "Invalid null token value"); + return p.getTokenFactory().fromByteArray(value); + } + + /** + * Checks if the query does not contains any restriction on the clustering columns. + * + * @return true if the query does not contains any restriction on the clustering columns, + * false otherwise. + */ + public boolean hasNoClusteringColumnsRestriction() + { + return clusteringColumnsRestrictions.isEmpty(); + } + + /** + * Checks if the query has some restrictions on the clustering columns. + * + * @return true if the query has some restrictions on the clustering columns, + * false otherwise. + */ + public boolean hasClusteringColumnsRestriction() + { + return !clusteringColumnsRestrictions.isEmpty(); + } + + // For non-composite slices, we don't support internally the difference between exclusive and + // inclusive bounds, so we deal with it manually. + public boolean isNonCompositeSliceWithExclusiveBounds() + { + return !cfm.comparator.isCompound() + && clusteringColumnsRestrictions.isSlice() + && (!clusteringColumnsRestrictions.isInclusive(Bound.START) || !clusteringColumnsRestrictions.isInclusive(Bound.END)); + } + + /** + * Returns the requested clustering columns as Composites. + * + * @param options the query options + * @return the requested clustering columns as Composites + * @throws InvalidRequestException if the query is not valid + */ + public List getClusteringColumnsAsComposites(QueryOptions options) throws InvalidRequestException + { + return clusteringColumnsRestrictions.valuesAsComposites(options); + } + + /** + * Returns the bounds (start or end) of the clustering columns as Composites. + * + * @param b the bound type + * @param options the query options + * @return the bounds (start or end) of the clustering columns as Composites + * @throws InvalidRequestException if the request is not valid + */ + public List getClusteringColumnsBoundsAsComposites(Bound b, + QueryOptions options) throws InvalidRequestException + { + List bounds = clusteringColumnsRestrictions.boundsAsComposites(b, options); + for (Composite c : bounds) { + if (!c.isEmpty()) + QueryProcessor.validateComposite(c, cfm.comparator); + } + return bounds; + } + + /** + * Returns the bounds (start or end) of the clustering columns. + * + * @param b the bound type + * @param options the query options + * @return the bounds (start or end) of the clustering columns + * @throws InvalidRequestException if the request is not valid + */ + public List getClusteringColumnsBounds(Bound b, QueryOptions options) throws InvalidRequestException + { + return clusteringColumnsRestrictions.bounds(b, options); + } + + /** + * Checks if the bounds (start or end) of the clustering columns are inclusive. + * + * @param bound the bound type + * @return true if the bounds (start or end) of the clustering columns are inclusive, + * false otherwise + */ + public boolean areRequestedBoundsInclusive(Bound bound) + { + return clusteringColumnsRestrictions.isInclusive(bound); + } + + /** + * Checks if the query returns a range of columns. + * + * @return true if the query returns a range of columns, false otherwise. + */ + public boolean isColumnRange() + { + // Due to CASSANDRA-5762, we always do a slice for CQL3 tables (not dense, composite). + // Static CF (non dense but non composite) never entails a column slice however + if (!cfm.comparator.isDense()) + return cfm.comparator.isCompound(); + + // Otherwise (i.e. for compact table where we don't have a row marker anyway and thus don't care about + // CASSANDRA-5762), + // it is a range query if it has at least one the column alias for which no relation is defined or is not EQ. + return clusteringColumnsRestrictions.size() < cfm.clusteringColumns().size() || clusteringColumnsRestrictions.isSlice(); + } + + /** + * Checks if the query need to use filtering. + * @return true if the query need to use filtering, false otherwise. + */ + public boolean needFiltering() + { + int numberOfRestrictedColumns = 0; + for (Restrictions restrictions : indexRestrictions) + numberOfRestrictedColumns += restrictions.size(); + + return numberOfRestrictedColumns > 1 + || (numberOfRestrictedColumns == 0 && !clusteringColumnsRestrictions.isEmpty()) + || (numberOfRestrictedColumns != 0 + && nonPrimaryKeyRestrictions.hasMultipleContains()); + } + + private void validateSecondaryIndexSelections(boolean selectsOnlyStaticColumns) throws InvalidRequestException + { + checkFalse(keyIsInRelation(), + "Select on indexed columns and with IN clause for the PRIMARY KEY are not supported"); + // When the user only select static columns, the intent is that we don't query the whole partition but just + // the static parts. But 1) we don't have an easy way to do that with 2i and 2) since we don't support index on + // static columns + // so far, 2i means that you've restricted a non static column, so the query is somewhat non-sensical. + checkFalse(selectsOnlyStaticColumns, "Queries using 2ndary indexes don't support selecting only static columns"); + } + + public void reverse() + { + clusteringColumnsRestrictions = new ReversedPrimaryKeyRestrictions(clusteringColumnsRestrictions); + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/e06dae81/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java ---------------------------------------------------------------------- diff --cc test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java index a1542ce,98dda26..0975662 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectMultiColumnRelationTest.java @@@ -79,39 -81,43 +79,39 @@@ public class SelectMultiColumnRelationT "SELECT * FROM %s WHERE a = 0 AND (b, c, d) IN ((?, ?, ?))", 1, 2, null); // Wrong type for 'd' - assertInvalidMessage("Expected 4 or 0 byte int (6)", - "SELECT * FROM %s WHERE a = 0 AND (b, c, d) = (?, ?, ?)", 1, 2, "foobar"); - - assertInvalidMessage("Invalid tuple type literal for b of type int", - "SELECT * FROM %s WHERE a = 0 AND b = (?, ?, ?)", 1, 2, 3); + assertInvalid("SELECT * FROM %s WHERE a = 0 AND (b, c, d) = (?, ?, ?)", 1, 2, "foobar"); + assertInvalid("SELECT * FROM %s WHERE a = 0 AND b = (?, ?, ?)", 1, 2, 3); // Mix single and tuple inequalities - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: c < ?", + assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = 0 AND (b, c, d) > (?, ?, ?) AND c < ?", 0, 1, 0, 1); - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: (b, c, d) < (?, ?, ?)", + assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = 0 AND c > ? AND (b, c, d) < (?, ?, ?)", 1, 1, 1, 0); - assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + assertInvalidMessage("Multi-column relations can only be applied to clustering columns but was applied to: a", "SELECT * FROM %s WHERE (a, b, c, d) IN ((?, ?, ?, ?))", 0, 1, 2, 3); - assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted (preceding column \"b\" is not restricted)", + assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted as preceding column \"b\" is not restricted", "SELECT * FROM %s WHERE (c, d) IN ((?, ?))", 0, 1); - assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", + + assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", "SELECT * FROM %s WHERE a = ? AND b > ? AND (c, d) IN ((?, ?))", 0, 0, 0, 0); - assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", + assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", "SELECT * FROM %s WHERE a = ? AND b > ? AND (c, d) > (?, ?)", 0, 0, 0, 0); -- assertInvalidMessage("PRIMARY KEY column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", ++ assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", "SELECT * FROM %s WHERE a = ? AND (c, d) > (?, ?) AND b > ? ", 0, 0, 0, 0); - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: (c) < (?)", + + assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = ? AND (b, c) > (?, ?) AND (b) < (?) AND (c) < (?)", 0, 0, 0, 0, 0); - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: (b, c) > (?, ?)", + assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = ? AND (c) < (?) AND (b, c) > (?, ?) AND (b) < (?)", 0, 0, 0, 0, 0); - assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: (b, c) > (?, ?)", ++ assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = ? AND (b) < (?) AND (c) < (?) AND (b, c) > (?, ?)", 0, 0, 0, 0, 0); - assertInvalidMessage("Clustering column \"c\" cannot be restricted (preceding column \"b\" is restricted by a non-EQ relation)", - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: (b, c) > (?, ?)", ++ assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = ? AND (b) < (?) AND c < ? AND (b, c) > (?, ?)", 0, 0, 0, 0, 0); - assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column: (c) < (?)", + assertInvalidMessage("Column \"c\" cannot be restricted by two inequalities not starting with the same column", "SELECT * FROM %s WHERE a = ? AND (b, c) > (?, ?) AND (c) < (?)", 0, 0, 0, 0); - - assertInvalidMessage("PRIMARY KEY column \"d\" cannot be restricted (preceding column \"c\" is restricted by an IN tuple notation)", - "SELECT * FROM %s WHERE a = ? AND (b, c) in ((?, ?), (?, ?)) AND d > ?", 0, 0, 0, 0, 0, 0); } } @@@ -862,16 -786,12 +862,42 @@@ row(0, 0, 1, 1, 2), row(0, 1, 1, 1, 2)); - assertRows(execute("SELECT * FROM %s WHERE (b) >= (?) AND e = ?", 1, 2), + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT * FROM %s WHERE (b) >= (?) AND e = ?", 1, 2); + assertRows(execute("SELECT * FROM %s WHERE (b) >= (?) AND e = ? ALLOW FILTERING", 1, 2), row(0, 1, 1, 1, 2)); - assertRows(execute("SELECT * FROM %s WHERE (b, c) >= (?, ?) AND e = ?", 1, 1, 2), + assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, + "SELECT * FROM %s WHERE (b, c) >= (?, ?) AND e = ?", 1, 1, 2); + assertRows(execute("SELECT * FROM %s WHERE (b, c) >= (?, ?) AND e = ? ALLOW FILTERING", 1, 1, 2), row(0, 1, 1, 1, 2)); ++ } + ++ @Test ++ public void testMultiColumnRestrictionsWithIndex() throws Throwable ++ { ++ createTable("CREATE TABLE %s (a int, b int, c int, d int, e int, v int, PRIMARY KEY (a, b, c, d, e))"); ++ createIndex("CREATE INDEX ON %s (v)"); ++ for (int i = 1; i <= 5; i++) ++ { ++ execute("INSERT INTO %s (a,b,c,d,e,v) VALUES (?,?,?,?,?,?)", 0, i, 0, 0, 0, 0); ++ execute("INSERT INTO %s (a,b,c,d,e,v) VALUES (?,?,?,?,?,?)", 0, i, i, 0, 0, 0); ++ execute("INSERT INTO %s (a,b,c,d,e,v) VALUES (?,?,?,?,?,?)", 0, i, i, i, 0, 0); ++ execute("INSERT INTO %s (a,b,c,d,e,v) VALUES (?,?,?,?,?,?)", 0, i, i, i, i, 0); ++ execute("INSERT INTO %s (a,b,c,d,e,v) VALUES (?,?,?,?,?,?)", 0, i, i, i, i, i); ++ } + ++ String errorMsg = "Multi-column slice restrictions cannot be used for filtering."; ++ assertInvalidMessage(errorMsg, ++ "SELECT * FROM %s WHERE a = 0 AND (c,d) < (2,2) AND v = 0 ALLOW FILTERING"); ++ assertInvalidMessage(errorMsg, ++ "SELECT * FROM %s WHERE a = 0 AND (d,e) < (2,2) AND b = 1 AND v = 0 ALLOW FILTERING"); ++ assertInvalidMessage(errorMsg, ++ "SELECT * FROM %s WHERE a = 0 AND b = 1 AND (d,e) < (2,2) AND v = 0 ALLOW FILTERING"); ++ assertInvalidMessage(errorMsg, ++ "SELECT * FROM %s WHERE a = 0 AND b > 1 AND (d,e) < (2,2) AND v = 0 ALLOW FILTERING"); ++ assertInvalidMessage(errorMsg, ++ "SELECT * FROM %s WHERE a = 0 AND (b,c) > (1,0) AND (d,e) < (2,2) AND v = 0 ALLOW FILTERING"); } @Test http://git-wip-us.apache.org/repos/asf/cassandra/blob/e06dae81/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java ---------------------------------------------------------------------- diff --cc test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java index 9b10d0e,68cf6f8..c8df4c3 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/SelectTest.java @@@ -2278,4 -1264,68 +2278,65 @@@ public class SelectTest extends CQLTest "SELECT * FROM %s WHERE a = 'foo' AND b= 'bar' AND c IN (?, ?)", new String(TOO_BIG.array()), new String(TOO_BIG.array())); } + + @Test + public void testFilteringWithSecondaryIndex() throws Throwable + { + createTable("CREATE TABLE %s (pk int, " + + "c1 int, " + + "c2 int, " + + "c3 int, " + + "v int, " + + "PRIMARY KEY (pk, c1, c2, c3))"); + createIndex("CREATE INDEX v_idx_1 ON %s (v);"); + + for (int i = 1; i <= 5; i++) + { + execute("INSERT INTO %s (pk, c1, c2, c3, v) VALUES (?, ?, ?, ?, ?)", 1, 1, 1, 1, i); + execute("INSERT INTO %s (pk, c1, c2, c3, v) VALUES (?, ?, ?, ?, ?)", 1, 1, 1, i, i); + execute("INSERT INTO %s (pk, c1, c2, c3, v) VALUES (?, ?, ?, ?, ?)", 1, 1, i, i, i); + execute("INSERT INTO %s (pk, c1, c2, c3, v) VALUES (?, ?, ?, ?, ?)", 1, i, i, i, i); + } + + assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND c1 > 0 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;"), + row(1, 1, 1, 3, 3)); + + assertEmpty(execute("SELECT * FROM %s WHERE pk = 1 AND c1 > 1 AND c1 < 5 AND c2 = 1 AND v = 3 ALLOW FILTERING;")); + + assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND c1 > 1 AND c2 > 2 AND c3 > 2 AND v = 3 ALLOW FILTERING;"), + row(1, 3, 3, 3, 3)); + + assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND c1 > 1 AND c2 > 2 AND c3 = 3 AND v = 3 ALLOW FILTERING;"), + row(1, 3, 3, 3, 3)); + - assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND (c1, c2) > (1, 3) AND c3 = 3 AND v = 3 ALLOW FILTERING;"), - row(1, 3, 3, 3, 3)); - - assertInvalidMessage("Clustering column \"c1\" cannot be restricted by an IN relation", - "SELECT * FROM %s WHERE pk = 1 AND c1 IN(0,1,2) AND c2 = 1 AND v = 3 ALLOW FILTERING;"); ++ assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND c1 IN(0,1,2) AND c2 = 1 AND v = 3 ALLOW FILTERING;"), ++ row(1, 1, 1, 3, 3)); + - assertInvalidMessage("Clustering column \"c1\" cannot be restricted by an IN relation", - "SELECT * FROM %s WHERE pk = 1 AND c1 IN(0,1,2) AND c2 = 1 AND v = 3"); ++ assertRows(execute("SELECT * FROM %s WHERE pk = 1 AND c1 IN(0,1,2) AND c2 = 1 AND v = 3"), ++ row(1, 1, 1, 3, 3)); + - assertInvalidMessage("PRIMARY KEY column \"c2\" cannot be restricted (preceding column \"c1\" is restricted by a non-EQ relation)", ++ assertInvalidMessage("Clustering column \"c2\" cannot be restricted (preceding column \"c1\" is restricted by a non-EQ relation)", + "SELECT * FROM %s WHERE pk = 1 AND c1 > 0 AND c1 < 5 AND c2 = 1 ALLOW FILTERING;"); + - assertInvalidMessage("PRIMARY KEY column \"c2\" cannot be restricted (preceding column \"c1\" is not restricted)", ++ assertInvalidMessage("PRIMARY KEY column \"c2\" cannot be restricted as preceding column \"c1\" is not restricted", + "SELECT * FROM %s WHERE pk = 1 AND c2 = 1 ALLOW FILTERING;"); + } + + @Test + public void testIndexQueryWithCompositePartitionKey() throws Throwable + { + createTable("CREATE TABLE %s (p1 int, p2 int, v int, PRIMARY KEY ((p1, p2)))"); - assertInvalidMessage("Partition key part p2 must be restricted since preceding part is", ++ assertInvalidMessage("Partition key parts: p2 must be restricted as other parts are", + "SELECT * FROM %s WHERE p1 = 1 AND v = 3 ALLOW FILTERING"); + + createIndex("CREATE INDEX ON %s(v)"); + + execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 1, 1, 3); + execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 1, 2, 3); + execute("INSERT INTO %s(p1, p2, v) values (?, ?, ?)", 2, 1, 3); + + assertRows(execute("SELECT * FROM %s WHERE p1 = 1 AND v = 3 ALLOW FILTERING"), + row(1, 2, 3), + row(1, 1, 3)); + } }