Allow range deletions in CQL patch by Benjamin Lerer; reviewed by Joshua McKenzie for CASSANDRA-6237
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/2e3727e3 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/2e3727e3 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/2e3727e3 Branch: refs/heads/cassandra-3.0 Commit: 2e3727e3ff682dbab734aaccf641360bc62a8561 Parents: 8f249a6 Author: blerer <benjamin.le...@datastax.com> Authored: Fri Sep 4 21:10:29 2015 +0200 Committer: blerer <benjamin.le...@datastax.com> Committed: Fri Sep 4 21:10:29 2015 +0200 ---------------------------------------------------------------------- NEWS.txt | 6 +- doc/cql3/CQL.textile | 13 +- .../cassandra/config/ColumnDefinition.java | 6 +- .../cassandra/cql3/AbstractConditions.java | 65 ++ .../apache/cassandra/cql3/ColumnConditions.java | 167 ++++ .../apache/cassandra/cql3/ColumnIdentifier.java | 56 +- .../org/apache/cassandra/cql3/Conditions.java | 100 +++ src/java/org/apache/cassandra/cql3/Cql.g | 6 +- .../cassandra/cql3/IfExistsCondition.java | 36 + .../cassandra/cql3/IfNotExistsCondition.java | 36 + src/java/org/apache/cassandra/cql3/Json.java | 36 +- .../org/apache/cassandra/cql3/Operations.java | 135 ++++ .../cassandra/cql3/SingleColumnRelation.java | 1 + .../apache/cassandra/cql3/UpdateParameters.java | 9 +- .../restrictions/StatementRestrictions.java | 246 ++++-- .../cql3/statements/BatchStatement.java | 91 +-- .../cql3/statements/CQL3CasRequest.java | 14 +- .../cql3/statements/DeleteStatement.java | 99 +-- .../cql3/statements/ModificationStatement.java | 765 +++++++++---------- .../cql3/statements/SelectStatement.java | 9 +- .../cql3/statements/StatementType.java | 138 ++++ .../cql3/statements/UpdateStatement.java | 198 +++-- .../cql3/statements/UpdatesCollector.java | 130 ++++ src/java/org/apache/cassandra/db/CBuilder.java | 9 +- .../org/apache/cassandra/db/RangeTombstone.java | 4 +- src/java/org/apache/cassandra/db/Slices.java | 9 + .../cassandra/io/sstable/CQLSSTableWriter.java | 7 +- .../cassandra/cql3/MaterializedViewTest.java | 33 + .../cql3/validation/entities/UFAuthTest.java | 8 +- .../entities/UFIdentificationTest.java | 44 +- .../cql3/validation/operations/BatchTest.java | 79 +- .../cql3/validation/operations/DeleteTest.java | 681 ++++++++++++++++- .../cql3/validation/operations/InsertTest.java | 233 ++++++ .../operations/InsertUpdateIfConditionTest.java | 28 +- .../cql3/validation/operations/UpdateTest.java | 447 +++++++++++ .../cassandra/db/RangeTombstoneListTest.java | 222 +++++- 36 files changed, 3384 insertions(+), 782 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/NEWS.txt ---------------------------------------------------------------------- diff --git a/NEWS.txt b/NEWS.txt index c7976b9..af2f64c 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -18,6 +18,11 @@ using the provided 'sstableupgrade' tool. New features ------------ + - Support for IN restrictions on any partition key component or clustering key + as well as support for EQ and IN multicolumn restrictions has been added to + UPDATE and DELETE statement. + - Support for single-column and multi-colum slice restrictions (>, >=, <= and <) + has been added to DELETE statements - nodetool rebuild_index accepts the index argument without the redundant table name - Materialized Views, which allow for server-side denormalization, is now @@ -35,7 +40,6 @@ New features you do not run repair for a long time, you will keep all tombstones around which can cause other problems. - Upgrading --------- - Max mutation size is now configurable via max_mutation_size_in_kb setting in http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/doc/cql3/CQL.textile ---------------------------------------------------------------------- diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 74ed64e..0e04528 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -863,8 +863,11 @@ bc(syntax).. <where-clause> ::= <relation> ( AND <relation> )* <relation> ::= <identifier> '=' <term> - | <identifier> IN '(' ( <term> ( ',' <term> )* )? ')' + | '(' <identifier> (',' <identifier>)* ')' '=' <term-tuple> + | <identifier> IN '(' ( <term> ( ',' <term>)* )? ')' | <identifier> IN '?' + | '(' <identifier> (',' <identifier>)* ')' IN '(' ( <term-tuple> ( ',' <term-tuple>)* )? ')' + | '(' <identifier> (',' <identifier>)* ')' IN '?' <option> ::= TIMESTAMP <integer> | TTL <integer> @@ -914,10 +917,14 @@ bc(syntax).. <where-clause> ::= <relation> ( AND <relation> )* -<relation> ::= <identifier> '=' <term> - | <identifier> IN '(' ( <term> ( ',' <term> )* )? ')' +<relation> ::= <identifier> <op> <term> + | '(' <identifier> (',' <identifier>)* ')' <op> <term-tuple> + | <identifier> IN '(' ( <term> ( ',' <term>)* )? ')' | <identifier> IN '?' + | '(' <identifier> (',' <identifier>)* ')' IN '(' ( <term-tuple> ( ',' <term-tuple>)* )? ')' + | '(' <identifier> (',' <identifier>)* ')' IN '?' +<op> ::= '=' | '<' | '>' | '<=' | '>=' <condition> ::= <identifier> '=' <term> | <identifier> '[' <term> ']' '=' <term> p. http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/config/ColumnDefinition.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/config/ColumnDefinition.java b/src/java/org/apache/cassandra/config/ColumnDefinition.java index 6afd3e7..82f2556 100644 --- a/src/java/org/apache/cassandra/config/ColumnDefinition.java +++ b/src/java/org/apache/cassandra/config/ColumnDefinition.java @@ -23,7 +23,7 @@ import java.util.*; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Objects; -import com.google.common.collect.Lists; +import com.google.common.collect.Collections2; import org.apache.cassandra.cql3.*; import org.apache.cassandra.db.rows.*; @@ -285,9 +285,9 @@ public class ColumnDefinition extends ColumnSpecification implements Comparable< * @param definitions the column definitions to convert. * @return the column identifiers corresponding to the specified definitions */ - public static List<ColumnIdentifier> toIdentifiers(List<ColumnDefinition> definitions) + public static Collection<ColumnIdentifier> toIdentifiers(Collection<ColumnDefinition> definitions) { - return Lists.transform(definitions, new Function<ColumnDefinition, ColumnIdentifier>() + return Collections2.transform(definitions, new Function<ColumnDefinition, ColumnIdentifier>() { @Override public ColumnIdentifier apply(ColumnDefinition columnDef) http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/AbstractConditions.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/AbstractConditions.java b/src/java/org/apache/cassandra/cql3/AbstractConditions.java new file mode 100644 index 0000000..71e3595 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/AbstractConditions.java @@ -0,0 +1,65 @@ +/* + * 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; + +import java.util.Collections; + +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.cql3.functions.Function; + +/** + * Base class for <code>Conditions</code> classes. + * + */ +abstract class AbstractConditions implements Conditions +{ + public Iterable<Function> getFunctions() + { + return Collections.emptyList(); + } + + public Iterable<ColumnDefinition> getColumns() + { + return null; + } + + public boolean isEmpty() + { + return false; + } + + public boolean appliesToStaticColumns() + { + return false; + } + + public boolean appliesToRegularColumns() + { + return false; + } + + public boolean isIfExists() + { + return false; + } + + public boolean isIfNotExists() + { + return false; + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/ColumnConditions.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/ColumnConditions.java b/src/java/org/apache/cassandra/cql3/ColumnConditions.java new file mode 100644 index 0000000..5ac8119 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/ColumnConditions.java @@ -0,0 +1,167 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.statements.CQL3CasRequest; +import org.apache.cassandra.db.Clustering; + +import static java.util.stream.StreamSupport.stream; + +/** + * A set of <code>ColumnCondition</code>s. + * + */ +public final class ColumnConditions extends AbstractConditions +{ + /** + * The conditions on regular columns. + */ + private final List<ColumnCondition> columnConditions; + + /** + * The conditions on static columns + */ + private final List<ColumnCondition> staticConditions; + + /** + * Creates a new <code>ColumnConditions</code> instance for the specified builder. + */ + private ColumnConditions(Builder builder) + { + this.columnConditions = builder.columnConditions; + this.staticConditions = builder.staticConditions; + } + + @Override + public boolean appliesToStaticColumns() + { + return !staticConditions.isEmpty(); + } + + @Override + public boolean appliesToRegularColumns() + { + return !columnConditions.isEmpty(); + } + + @Override + public Collection<ColumnDefinition> getColumns() + { + return Stream.concat(columnConditions.stream(), staticConditions.stream()) + .map(e -> e.column) + .collect(Collectors.toList()); + } + + @Override + public boolean isEmpty() + { + return columnConditions.isEmpty() && staticConditions.isEmpty(); + } + + /** + * Adds the conditions to the specified CAS request. + * + * @param request the request + * @param clustering the clustering prefix + * @param options the query options + */ + public void addConditionsTo(CQL3CasRequest request, + Clustering clustering, + QueryOptions options) + { + if (!columnConditions.isEmpty()) + request.addConditions(clustering, columnConditions, options); + if (!staticConditions.isEmpty()) + request.addConditions(Clustering.STATIC_CLUSTERING, staticConditions, options); + } + + @Override + public Iterable<Function> getFunctions() + { + return Stream.concat(columnConditions.stream(), staticConditions.stream()) + .flatMap(e -> stream(e.getFunctions().spliterator(), false)) + .collect(Collectors.toList()); + } + + /** + * Creates a new <code>Builder</code> for <code>ColumnConditions</code>. + * @return a new <code>Builder</code> for <code>ColumnConditions</code> + */ + public static Builder newBuilder() + { + return new Builder(); + } + + /** + * A <code>Builder</code> for <code>ColumnConditions</code>. + * + */ + public static final class Builder + { + /** + * The conditions on regular columns. + */ + private List<ColumnCondition> columnConditions = Collections.emptyList(); + + /** + * The conditions on static columns + */ + private List<ColumnCondition> staticConditions = Collections.emptyList(); + + /** + * Adds the specified <code>ColumnCondition</code> to this set of conditions. + * @param condition the condition to add + */ + public Builder add(ColumnCondition condition) + { + List<ColumnCondition> conds = null; + if (condition.column.isStatic()) + { + if (staticConditions.isEmpty()) + staticConditions = new ArrayList<>(); + conds = staticConditions; + } + else + { + if (columnConditions.isEmpty()) + columnConditions = new ArrayList<>(); + conds = columnConditions; + } + conds.add(condition); + return this; + } + + public ColumnConditions build() + { + return new ColumnConditions(this); + } + + private Builder() + { + } + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java index 47e4384..6102bb9 100644 --- a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java +++ b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java @@ -194,12 +194,18 @@ public class ColumnIdentifier extends org.apache.cassandra.cql3.selection.Select * once the comparator is known with prepare(). This should only be used with identifiers that are actual * column names. See CASSANDRA-8178 for more background. */ - public static class Raw implements Selectable.Raw + public static interface Raw extends Selectable.Raw + { + + public ColumnIdentifier prepare(CFMetaData cfm); + } + + public static class Literal implements Raw { private final String rawText; private final String text; - public Raw(String rawText, boolean keepCase) + public Literal(String rawText, boolean keepCase) { this.rawText = rawText; this.text = keepCase ? rawText : rawText.toLowerCase(Locale.US); @@ -239,9 +245,10 @@ public class ColumnIdentifier extends org.apache.cassandra.cql3.selection.Select @Override public final boolean equals(Object o) { - if(!(o instanceof ColumnIdentifier.Raw)) + if(!(o instanceof Literal)) return false; - ColumnIdentifier.Raw that = (ColumnIdentifier.Raw)o; + + Literal that = (Literal) o; return text.equals(that.text); } @@ -251,4 +258,45 @@ public class ColumnIdentifier extends org.apache.cassandra.cql3.selection.Select return text; } } + + public static class ColumnIdentifierValue implements Raw + { + private final ColumnIdentifier identifier; + + public ColumnIdentifierValue(ColumnIdentifier identifier) + { + this.identifier = identifier; + } + + public ColumnIdentifier prepare(CFMetaData cfm) + { + return identifier; + } + + public boolean processesSelection() + { + return false; + } + + @Override + public final int hashCode() + { + return identifier.hashCode(); + } + + @Override + public final boolean equals(Object o) + { + if(!(o instanceof ColumnIdentifierValue)) + return false; + ColumnIdentifierValue that = (ColumnIdentifierValue) o; + return identifier.equals(that.identifier); + } + + @Override + public String toString() + { + return identifier.toString(); + } + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/Conditions.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Conditions.java b/src/java/org/apache/cassandra/cql3/Conditions.java new file mode 100644 index 0000000..85459c4 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Conditions.java @@ -0,0 +1,100 @@ +/* + * 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; + +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.statements.CQL3CasRequest; +import org.apache.cassandra.db.Clustering; + +/** + * Conditions that can be applied to a mutation statement. + * + */ +public interface Conditions +{ + /** + * An EMPTY condition + */ + static final Conditions EMPTY_CONDITION = ColumnConditions.newBuilder().build(); + + /** + * IF EXISTS condition + */ + static final Conditions IF_EXISTS_CONDITION = new IfExistsCondition(); + + /** + * IF NOT EXISTS condition + */ + static final Conditions IF_NOT_EXISTS_CONDITION = new IfNotExistsCondition(); + + /** + * Returns the functions used by the conditions. + * @return the functions used by the conditions + */ + Iterable<Function> getFunctions(); + + /** + * Returns the column definitions to which apply the conditions. + * @return the column definitions to which apply the conditions. + */ + Iterable<ColumnDefinition> getColumns(); + + /** + * Checks if this <code>Conditions</code> is empty. + * @return <code>true</code> if this <code>Conditions</code> is empty, <code>false</code> otherwise. + */ + boolean isEmpty(); + + /** + * Checks if this is a IF EXIST condition. + * @return <code>true</code> if this is a IF EXIST condition, <code>false</code> otherwise. + */ + boolean isIfExists(); + + /** + * Checks if this is a IF NOT EXIST condition. + * @return <code>true</code> if this is a IF NOT EXIST condition, <code>false</code> otherwise. + */ + boolean isIfNotExists(); + + /** + * Checks if some of the conditions apply to static columns. + * + * @return <code>true</code> if some of the conditions apply to static columns, <code>false</code> otherwise. + */ + boolean appliesToStaticColumns(); + + /** + * Checks if some of the conditions apply to regular columns. + * + * @return <code>true</code> if some of the conditions apply to regular columns, <code>false</code> otherwise. + */ + boolean appliesToRegularColumns(); + + /** + * Adds the conditions to the specified CAS request. + * + * @param request the request + * @param clustering the clustering prefix + * @param options the query options + */ + public void addConditionsTo(CQL3CasRequest request, + Clustering clustering, + QueryOptions options); +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/Cql.g ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g index 2149f10..87bec4b 100644 --- a/src/java/org/apache/cassandra/cql3/Cql.g +++ b/src/java/org/apache/cassandra/cql3/Cql.g @@ -1154,9 +1154,9 @@ userPassword[RoleOptions opts] // identifiers because the underlying comparator is not necessarily text. See // CASSANDRA-8178 for details. cident returns [ColumnIdentifier.Raw id] - : t=IDENT { $id = new ColumnIdentifier.Raw($t.text, false); } - | t=QUOTED_NAME { $id = new ColumnIdentifier.Raw($t.text, true); } - | k=unreserved_keyword { $id = new ColumnIdentifier.Raw(k, false); } + : t=IDENT { $id = new ColumnIdentifier.Literal($t.text, false); } + | t=QUOTED_NAME { $id = new ColumnIdentifier.Literal($t.text, true); } + | k=unreserved_keyword { $id = new ColumnIdentifier.Literal(k, false); } ; // Column identifiers where the comparator is known to be text http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/IfExistsCondition.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/IfExistsCondition.java b/src/java/org/apache/cassandra/cql3/IfExistsCondition.java new file mode 100644 index 0000000..a24d8c0 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/IfExistsCondition.java @@ -0,0 +1,36 @@ +/* + * 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; + +import org.apache.cassandra.cql3.statements.CQL3CasRequest; +import org.apache.cassandra.db.Clustering; + +final class IfExistsCondition extends AbstractConditions +{ + @Override + public void addConditionsTo(CQL3CasRequest request, Clustering clustering, QueryOptions options) + { + request.addExist(clustering); + } + + @Override + public boolean isIfExists() + { + return true; + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/IfNotExistsCondition.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/IfNotExistsCondition.java b/src/java/org/apache/cassandra/cql3/IfNotExistsCondition.java new file mode 100644 index 0000000..05cb864 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/IfNotExistsCondition.java @@ -0,0 +1,36 @@ +/* + * 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; + +import org.apache.cassandra.cql3.statements.CQL3CasRequest; +import org.apache.cassandra.db.Clustering; + +final class IfNotExistsCondition extends AbstractConditions +{ + @Override + public void addConditionsTo(CQL3CasRequest request, Clustering clustering, QueryOptions options) + { + request.addNotExist(clustering); + } + + @Override + public boolean isIfNotExists() + { + return true; + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/Json.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Json.java b/src/java/org/apache/cassandra/cql3/Json.java index e4bce29..35c69ed 100644 --- a/src/java/org/apache/cassandra/cql3/Json.java +++ b/src/java/org/apache/cassandra/cql3/Json.java @@ -71,7 +71,7 @@ public class Json public Prepared prepareAndCollectMarkers(CFMetaData metadata, Collection<ColumnDefinition> receivers, VariableSpecifications boundNames) { - return new PreparedLiteral(metadata.ksName, parseJson(text, receivers)); + return new PreparedLiteral(parseJson(text, receivers)); } } @@ -91,7 +91,7 @@ public class Json public Prepared prepareAndCollectMarkers(CFMetaData metadata, Collection<ColumnDefinition> receivers, VariableSpecifications boundNames) { boundNames.add(bindIndex, makeReceiver(metadata)); - return new PreparedMarker(metadata.ksName, bindIndex, receivers); + return new PreparedMarker(bindIndex, receivers); } private ColumnSpecification makeReceiver(CFMetaData metadata) @@ -105,27 +105,7 @@ public class Json */ public static abstract class Prepared { - private final String keyspace; - - protected Prepared(String keyspace) - { - this.keyspace = keyspace; - } - - protected abstract Term.Raw getRawTermForColumn(ColumnDefinition def); - - public Term getPrimaryKeyValueForColumn(ColumnDefinition def) - { - // Note that we know we don't have to call collectMarkerSpecification since it has already been collected - return getRawTermForColumn(def).prepare(keyspace, def); - } - - public Operation getSetOperationForColumn(ColumnDefinition def) - { - // Note that we know we don't have to call collectMarkerSpecification on the operation since we have - // already collected all we need. - return new Operation.SetValue(getRawTermForColumn(def)).prepare(keyspace, def); - } + public abstract Term.Raw getRawTermForColumn(ColumnDefinition def); } /** @@ -135,13 +115,12 @@ public class Json { private final Map<ColumnIdentifier, Term> columnMap; - public PreparedLiteral(String keyspace, Map<ColumnIdentifier, Term> columnMap) + public PreparedLiteral(Map<ColumnIdentifier, Term> columnMap) { - super(keyspace); this.columnMap = columnMap; } - protected Term.Raw getRawTermForColumn(ColumnDefinition def) + public Term.Raw getRawTermForColumn(ColumnDefinition def) { Term value = columnMap.get(def.name); return value == null ? Constants.NULL_LITERAL : new ColumnValue(value); @@ -158,14 +137,13 @@ public class Json private Map<ColumnIdentifier, Term> columnMap; - public PreparedMarker(String keyspace, int bindIndex, Collection<ColumnDefinition> columns) + public PreparedMarker(int bindIndex, Collection<ColumnDefinition> columns) { - super(keyspace); this.bindIndex = bindIndex; this.columns = columns; } - protected DelayedColumnValue getRawTermForColumn(ColumnDefinition def) + public DelayedColumnValue getRawTermForColumn(ColumnDefinition def) { return new DelayedColumnValue(this, def); } http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/Operations.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Operations.java b/src/java/org/apache/cassandra/cql3/Operations.java new file mode 100644 index 0000000..c4cade1 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Operations.java @@ -0,0 +1,135 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.cassandra.cql3.functions.Function; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + +/** + * A set of <code>Operation</code>s. + * + */ +public final class Operations implements Iterable<Operation> +{ + /** + * The operations on regular columns. + */ + private final List<Operation> regularOperations = new ArrayList<>(); + + /** + * The operations on static columns. + */ + private final List<Operation> staticOperations = new ArrayList<>(); + + /** + * Checks if some of the operations apply to static columns. + * + * @return <code>true</code> if some of the operations apply to static columns, <code>false</code> otherwise. + */ + public boolean appliesToStaticColumns() + { + return !staticOperations.isEmpty(); + } + + /** + * Checks if some of the operations apply to regular columns. + * + * @return <code>true</code> if some of the operations apply to regular columns, <code>false</code> otherwise. + */ + public boolean appliesToRegularColumns() + { + return !regularOperations.isEmpty(); + } + + /** + * Returns the operation on regular columns. + * @return the operation on regular columns + */ + public List<Operation> regularOperations() + { + return regularOperations; + } + + /** + * Returns the operation on static columns. + * @return the operation on static columns + */ + public List<Operation> staticOperations() + { + return staticOperations; + } + + /** + * Adds the specified <code>Operation</code> to this set of operations. + * @param operation the operation to add + */ + public void add(Operation operation) + { + if (operation.column.isStatic()) + staticOperations.add(operation); + else + regularOperations.add(operation); + } + + /** + * Checks if one of the operations requires a read. + * + * @return <code>true</code> if one of the operations requires a read, <code>false</code> otherwise. + */ + public boolean requiresRead() + { + // Lists SET operation incurs a read. + for (Operation operation : this) + if (operation.requiresRead()) + return true; + + return false; + } + + /** + * Checks if this <code>Operations</code> is empty. + * @return <code>true</code> if this <code>Operations</code> is empty, <code>false</code> otherwise. + */ + public boolean isEmpty() + { + return staticOperations.isEmpty() && regularOperations.isEmpty(); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator<Operation> iterator() + { + return Iterators.concat(staticOperations.iterator(), regularOperations.iterator()); + } + + public Iterable<? extends Function> getFunctions() + { + List<Function> functions = new ArrayList<>(); + for (Operation operation : this) + Iterables.addAll(functions, operation.getFunctions()); + return functions; + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java index c848b9e..84e6274 100644 --- a/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java +++ b/src/java/org/apache/cassandra/cql3/SingleColumnRelation.java @@ -223,6 +223,7 @@ public final class SingleColumnRelation extends Relation } checkFalse(isContainsKey() && !(receiver.type instanceof MapType), "Cannot use CONTAINS KEY on non-map column %s", receiver.name); + checkFalse(isContains() && !(receiver.type.isCollection()), "Cannot use CONTAINS on non-collection column %s", receiver.name); if (mapKey != null) { http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/UpdateParameters.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/UpdateParameters.java b/src/java/org/apache/cassandra/cql3/UpdateParameters.java index cd81f84..dbcf803 100644 --- a/src/java/org/apache/cassandra/cql3/UpdateParameters.java +++ b/src/java/org/apache/cassandra/cql3/UpdateParameters.java @@ -216,9 +216,14 @@ public class UpdateParameters return deletionTime; } - public RangeTombstone makeRangeTombstone(CBuilder cbuilder) + public RangeTombstone makeRangeTombstone(ClusteringComparator comparator, Clustering clustering) { - return new RangeTombstone(cbuilder.buildSlice(), deletionTime); + return makeRangeTombstone(Slice.make(comparator, clustering)); + } + + public RangeTombstone makeRangeTombstone(Slice slice) + { + return new RangeTombstone(slice, deletionTime); } public Row getPrefetchedRow(DecoratedKey key, Clustering clustering) http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java index b0c81b8..3cf6bfb 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java @@ -25,13 +25,16 @@ import com.google.common.collect.Iterables; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.ColumnDefinition; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.Relation; +import org.apache.cassandra.cql3.VariableSpecifications; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.Bound; +import org.apache.cassandra.cql3.statements.StatementType; import org.apache.cassandra.db.*; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.dht.*; -import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.index.SecondaryIndexManager; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.btree.BTreeSet; @@ -49,6 +52,11 @@ public final class StatementRestrictions public static final String NO_INDEX_FOUND_MESSAGE = "No supported secondary index found for the non primary key columns restrictions"; /** + * The type of statement + */ + private final StatementType type; + + /** * The Column Family meta data */ public final CFMetaData cfm; @@ -86,30 +94,33 @@ public final class StatementRestrictions /** * Creates a new empty <code>StatementRestrictions</code>. * + * @param type the type of statement * @param cfm the column family meta data * @return a new empty <code>StatementRestrictions</code>. */ - public static StatementRestrictions empty(CFMetaData cfm) + public static StatementRestrictions empty(StatementType type, CFMetaData cfm) { - return new StatementRestrictions(cfm); + return new StatementRestrictions(type, cfm); } - private StatementRestrictions(CFMetaData cfm) + private StatementRestrictions(StatementType type, CFMetaData cfm) { + this.type = type; this.cfm = cfm; this.partitionKeyRestrictions = new PrimaryKeyRestrictionSet(cfm.getKeyValidatorAsClusteringComparator(), true); this.clusteringColumnsRestrictions = new PrimaryKeyRestrictionSet(cfm.comparator, false); this.nonPrimaryKeyRestrictions = new RestrictionSet(); } - public StatementRestrictions(CFMetaData cfm, + public StatementRestrictions(StatementType type, + CFMetaData cfm, List<Relation> whereClause, VariableSpecifications boundNames, boolean selectsOnlyStaticColumns, boolean selectACollection, - boolean useFiltering) throws InvalidRequestException + boolean useFiltering) { - this(cfm); + this(type, cfm); /* * WHERE clause. For a given entity, rules are: @@ -123,13 +134,19 @@ public final class StatementRestrictions for (Relation relation : whereClause) addRestriction(relation.toRestriction(cfm, boundNames)); - ColumnFamilyStore cfs = Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfName); - SecondaryIndexManager secondaryIndexManager = cfs.indexManager; + boolean hasQueriableClusteringColumnIndex = false; + boolean hasQueriableIndex = false; - boolean hasQueriableClusteringColumnIndex = clusteringColumnsRestrictions.hasSupportingIndex(secondaryIndexManager); - boolean hasQueriableIndex = hasQueriableClusteringColumnIndex - || partitionKeyRestrictions.hasSupportingIndex(secondaryIndexManager) - || nonPrimaryKeyRestrictions.hasSupportingIndex(secondaryIndexManager); + if (type.allowUseOfSecondaryIndices()) + { + ColumnFamilyStore cfs = Keyspace.open(cfm.ksName).getColumnFamilyStore(cfm.cfName); + SecondaryIndexManager secondaryIndexManager = cfs.indexManager; + + hasQueriableClusteringColumnIndex = clusteringColumnsRestrictions.hasSupportingIndex(secondaryIndexManager); + 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); @@ -139,10 +156,26 @@ public final class StatementRestrictions if (usesSecondaryIndexing) indexRestrictions.add(partitionKeyRestrictions); - checkFalse(selectsOnlyStaticColumns && hasClusteringColumnsRestriction(), - "Cannot restrict clustering columns when selecting only static columns"); + if (selectsOnlyStaticColumns && hasClusteringColumnsRestriction()) + { + // If the only updated/deleted columns are static, then we don't need clustering columns. + // And in fact, unless it is an INSERT, we reject if clustering colums are provided as that + // suggest something unintended. For instance, given: + // CREATE TABLE t (k int, v int, s int static, PRIMARY KEY (k, v)) + // it can make sense to do: + // INSERT INTO t(k, v, s) VALUES (0, 1, 2) + // but both + // UPDATE t SET s = 3 WHERE k = 0 AND v = 1 + // DELETE v FROM t WHERE k = 0 AND v = 1 + // sounds like you don't really understand what your are doing. + if (type.isDelete() || type.isUpdate()) + throw invalidRequest("Invalid restrictions on clustering columns since the %s statement modifies only static columns", + type); + if (type.isSelect()) + throw invalidRequest("Cannot restrict clustering columns when selecting only static columns"); + } - processClusteringColumnsRestrictions(hasQueriableIndex, selectACollection); + processClusteringColumnsRestrictions(hasQueriableIndex, selectsOnlyStaticColumns, selectACollection); // Covers indexes on the first clustering column (among others). if (isKeyRange && hasQueriableClusteringColumnIndex) @@ -157,10 +190,18 @@ public final class StatementRestrictions // there is restrictions not covered by the PK. if (!nonPrimaryKeyRestrictions.isEmpty()) { + if (!type.allowNonPrimaryKeyInWhereClause()) + { + Collection<ColumnIdentifier> nonPrimaryKeyColumns = + ColumnDefinition.toIdentifiers(nonPrimaryKeyRestrictions.getColumnDefs()); + + throw invalidRequest("Non PRIMARY KEY columns found in where clause: %s ", + Joiner.on(", ").join(nonPrimaryKeyColumns)); + } if (hasQueriableIndex) usesSecondaryIndexing = true; else if (!useFiltering) - throw new InvalidRequestException(NO_INDEX_FOUND_MESSAGE); + throw invalidRequest(NO_INDEX_FOUND_MESSAGE); indexRestrictions.add(nonPrimaryKeyRestrictions); } @@ -169,7 +210,7 @@ public final class StatementRestrictions validateSecondaryIndexSelections(selectsOnlyStaticColumns); } - private void addRestriction(Restriction restriction) throws InvalidRequestException + private void addRestriction(Restriction restriction) { if (restriction.isMultiColumn()) clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction); @@ -186,7 +227,7 @@ public final class StatementRestrictions nonPrimaryKeyRestrictions.getFunctions()); } - private void addSingleColumnRestriction(SingleColumnRestriction restriction) throws InvalidRequestException + private void addSingleColumnRestriction(SingleColumnRestriction restriction) { ColumnDefinition def = restriction.columnDef; if (def.isPartitionKey()) @@ -241,8 +282,19 @@ public final class StatementRestrictions return this.usesSecondaryIndexing; } - private void processPartitionKeyRestrictions(boolean hasQueriableIndex) throws InvalidRequestException + private void processPartitionKeyRestrictions(boolean hasQueriableIndex) { + if (!type.allowPartitionKeyRanges()) + { + checkFalse(partitionKeyRestrictions.isOnToken(), + "The token function cannot be used in WHERE clauses for %s statements", type); + + if (hasUnrestrictedPartitionKeyComponents()) + throw invalidRequest("Some partition key parts are missing: %s", + Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents())); + } + else + { // 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 @@ -252,17 +304,18 @@ public final class StatementRestrictions if (partitionKeyRestrictions.isOnToken()) isKeyRange = true; - if (hasPartitionKeyUnrestrictedComponents()) - { - if (!partitionKeyRestrictions.isEmpty()) + if (hasUnrestrictedPartitionKeyComponents()) { - if (!hasQueriableIndex) - throw invalidRequest("Partition key parts: %s must be restricted as other parts are", - Joiner.on(", ").join(getPartitionKeyUnrestrictedComponents())); - } + 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; + isKeyRange = true; + usesSecondaryIndexing = hasQueriableIndex; + } } } @@ -270,7 +323,7 @@ public final class StatementRestrictions * Checks if the partition key has some unrestricted components. * @return <code>true</code> if the partition key has some unrestricted components, <code>false</code> otherwise. */ - private boolean hasPartitionKeyUnrestrictedComponents() + private boolean hasUnrestrictedPartitionKeyComponents() { return partitionKeyRestrictions.size() < cfm.partitionKeyColumns().size(); } @@ -284,7 +337,7 @@ public final class StatementRestrictions * Returns the partition key components that are not restricted. * @return the partition key components that are not restricted. */ - private List<ColumnIdentifier> getPartitionKeyUnrestrictedComponents() + private Collection<ColumnIdentifier> getPartitionKeyUnrestrictedComponents() { List<ColumnDefinition> list = new ArrayList<>(cfm.partitionKeyColumns()); list.removeAll(partitionKeyRestrictions.getColumnDefs()); @@ -292,39 +345,65 @@ public final class StatementRestrictions } /** + * Checks if the restrictions on the partition key are token restrictions. + * + * @return <code>true</code> if the restrictions on the partition key are token restrictions, + * <code>false</code> otherwise. + */ + public boolean isPartitionKeyRestrictionsOnToken() + { + return partitionKeyRestrictions.isOnToken(); + } + + /** * Processes the clustering column restrictions. * * @param hasQueriableIndex <code>true</code> if some of the queried data are indexed, <code>false</code> otherwise + * @param selectsOnlyStaticColumns <code>true</code> if the selected or modified columns are all statics, + * <code>false</code> otherwise. * @param selectACollection <code>true</code> if the query should return a collection column - * @throws InvalidRequestException if the request is invalid */ private void processClusteringColumnsRestrictions(boolean hasQueriableIndex, - boolean selectACollection) throws InvalidRequestException + boolean selectsOnlyStaticColumns, + boolean selectACollection) { - 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"); + checkFalse(!type.allowClusteringColumnSlices() && clusteringColumnsRestrictions.isSlice(), + "Slice restrictions are not supported on the clustering columns in %s statements", type); - if (hasClusteringColumnsRestriction()) + if (!type.allowClusteringColumnSlices() + && (!cfm.isCompactTable() || (cfm.isCompactTable() && !hasClusteringColumnsRestriction()))) { - List<ColumnDefinition> clusteringColumns = cfm.clusteringColumns(); - List<ColumnDefinition> restrictedColumns = new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs()); + if (!selectsOnlyStaticColumns && hasUnrestrictedClusteringColumns()) + throw invalidRequest("Some clustering keys are missing: %s", + Joiner.on(", ").join(getUnrestrictedClusteringColumns())); + } + else + { + 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"); - for (int i = 0, m = restrictedColumns.size(); i < m; i++) + if (hasClusteringColumnsRestriction()) { - ColumnDefinition clusteringColumn = clusteringColumns.get(i); - ColumnDefinition restrictedColumn = restrictedColumns.get(i); + List<ColumnDefinition> clusteringColumns = cfm.clusteringColumns(); + List<ColumnDefinition> restrictedColumns = new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs()); - if (!clusteringColumn.equals(restrictedColumn)) + for (int i = 0, m = restrictedColumns.size(); i < m; i++) { - checkTrue(hasQueriableIndex, - "PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted", - restrictedColumn.name, - clusteringColumn.name); - - usesSecondaryIndexing = true; // handle gaps and non-keyrange cases. - break; + ColumnDefinition clusteringColumn = clusteringColumns.get(i); + ColumnDefinition restrictedColumn = restrictedColumns.get(i); + + if (!clusteringColumn.equals(restrictedColumn)) + { + checkTrue(hasQueriableIndex, + "PRIMARY KEY column \"%s\" cannot be restricted as preceding column \"%s\" is not restricted", + restrictedColumn.name, + clusteringColumn.name); + + usesSecondaryIndexing = true; // handle gaps and non-keyrange cases. + break; + } } } } @@ -333,7 +412,27 @@ public final class StatementRestrictions usesSecondaryIndexing = true; } - public RowFilter getRowFilter(SecondaryIndexManager indexManager, QueryOptions options) throws InvalidRequestException + /** + * Returns the clustering columns that are not restricted. + * @return the clustering columns that are not restricted. + */ + private Collection<ColumnIdentifier> getUnrestrictedClusteringColumns() + { + List<ColumnDefinition> missingClusteringColumns = new ArrayList<>(cfm.clusteringColumns()); + missingClusteringColumns.removeAll(new LinkedList<>(clusteringColumnsRestrictions.getColumnDefs())); + return ColumnDefinition.toIdentifiers(missingClusteringColumns); + } + + /** + * Checks if some clustering columns are not restricted. + * @return <code>true</code> if some clustering columns are not restricted, <code>false</code> otherwise. + */ + private boolean hasUnrestrictedClusteringColumns() + { + return cfm.clusteringColumns().size() != clusteringColumnsRestrictions.size(); + } + + public RowFilter getRowFilter(SecondaryIndexManager indexManager, QueryOptions options) { if (indexRestrictions.isEmpty()) return RowFilter.NONE; @@ -350,9 +449,8 @@ public final class StatementRestrictions * * @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<ByteBuffer> getPartitionKeys(final QueryOptions options) throws InvalidRequestException + public List<ByteBuffer> getPartitionKeys(final QueryOptions options) { return partitionKeyRestrictions.values(options); } @@ -363,13 +461,12 @@ public final class StatementRestrictions * @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 + private ByteBuffer getPartitionKeyBound(Bound b, QueryOptions options) { // 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()) + if (hasUnrestrictedPartitionKeyComponents()) return ByteBufferUtil.EMPTY_BYTE_BUFFER; // We deal with IN queries for keys in other places, so we know buildBound will return only one result @@ -381,9 +478,8 @@ public final class StatementRestrictions * * @param options the query options * @return the partition key bounds - * @throws InvalidRequestException if the query is invalid */ - public AbstractBounds<PartitionPosition> getPartitionKeyBounds(QueryOptions options) throws InvalidRequestException + public AbstractBounds<PartitionPosition> getPartitionKeyBounds(QueryOptions options) { IPartitioner p = cfm.partitioner; @@ -396,7 +492,7 @@ public final class StatementRestrictions } private AbstractBounds<PartitionPosition> getPartitionKeyBounds(IPartitioner p, - QueryOptions options) throws InvalidRequestException + QueryOptions options) { ByteBuffer startKeyBytes = getPartitionKeyBound(Bound.START, options); ByteBuffer finishKeyBytes = getPartitionKeyBound(Bound.END, options); @@ -420,8 +516,7 @@ public final class StatementRestrictions } private AbstractBounds<PartitionPosition> getPartitionKeyBoundsForTokenRestrictions(IPartitioner p, - QueryOptions options) - throws InvalidRequestException + QueryOptions options) { Token startToken = getTokenBound(Bound.START, options, p); Token endToken = getTokenBound(Bound.END, options, p); @@ -450,7 +545,7 @@ public final class StatementRestrictions return new Range<>(start, end); } - private Token getTokenBound(Bound b, QueryOptions options, IPartitioner p) throws InvalidRequestException + private Token getTokenBound(Bound b, QueryOptions options, IPartitioner p) { if (!partitionKeyRestrictions.hasBound(b)) return p.getMinimumToken(); @@ -476,9 +571,8 @@ public final class StatementRestrictions * * @param options the query options * @return the requested clustering columns - * @throws InvalidRequestException if the query is not valid */ - public NavigableSet<Clustering> getClusteringColumns(QueryOptions options) throws InvalidRequestException + public NavigableSet<Clustering> getClusteringColumns(QueryOptions options) { // If this is a names command and the table is a static compact one, then as far as CQL is concerned we have // only a single row which internally correspond to the static parts. In which case we want to return an empty @@ -495,9 +589,8 @@ public final class StatementRestrictions * @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 NavigableSet<Slice.Bound> getClusteringColumnsBounds(Bound b, QueryOptions options) throws InvalidRequestException + public NavigableSet<Slice.Bound> getClusteringColumnsBounds(Bound b, QueryOptions options) { return clusteringColumnsRestrictions.boundsAsClustering(b, options); } @@ -546,7 +639,7 @@ public final class StatementRestrictions && nonPrimaryKeyRestrictions.hasMultipleContains()); } - private void validateSecondaryIndexSelections(boolean selectsOnlyStaticColumns) throws InvalidRequestException + private void validateSecondaryIndexSelections(boolean selectsOnlyStaticColumns) { checkFalse(keyIsInRelation(), "Select on indexed columns and with IN clause for the PRIMARY KEY are not supported"); @@ -556,4 +649,19 @@ public final class StatementRestrictions // 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"); } + + /** + * Checks that all the primary key columns (partition key and clustering columns) are restricted by an equality + * relation ('=' or 'IN'). + * + * @return <code>true</code> if all the primary key columns are restricted by an equality relation. + */ + public boolean hasAllPKColumnsRestrictedByEqualities() + { + return !isPartitionKeyRestrictionsOnToken() + && !hasUnrestrictedPartitionKeyComponents() + && (partitionKeyRestrictions.isEQ() || partitionKeyRestrictions.isIN()) + && !hasUnrestrictedClusteringColumns() + && (clusteringColumnsRestrictions.isEQ() || clusteringColumnsRestrictions.isIN()); + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java index c8482b3..4a92ec1 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -27,7 +27,6 @@ import com.google.common.collect.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.MessageFormatter; - import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.*; @@ -44,6 +43,8 @@ import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Pair; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; + /** * A <code>BATCH</code> statement parsed from a CQL query. */ @@ -217,8 +218,7 @@ public class BatchStatement implements CQLStatement throws RequestExecutionException, RequestValidationException { Set<String> tablesWithZeroGcGs = null; - - Map<String, Map<ByteBuffer, IMutation>> mutations = new HashMap<>(); + UpdatesCollector collector = new UpdatesCollector(updatedColumns, updatedRows()); for (int i = 0; i < statements.size(); i++) { ModificationStatement statement = statements.get(i); @@ -230,7 +230,7 @@ public class BatchStatement implements CQLStatement } QueryOptions statementOptions = options.forStatement(i); long timestamp = attrs.getTimestamp(now, statementOptions); - addStatementMutations(statement, statementOptions, local, timestamp, mutations); + statement.addUpdates(collector, statementOptions, local, timestamp); } if (tablesWithZeroGcGs != null) @@ -242,27 +242,7 @@ public class BatchStatement implements CQLStatement .getMessage()); } - return unzipMutations(mutations); - } - - private Collection<? extends IMutation> unzipMutations(Map<String, Map<ByteBuffer, IMutation>> mutations) - { - - // The case where all statement where on the same keyspace is pretty common - if (mutations.size() == 1) - return mutations.values().iterator().next().values(); - - - List<IMutation> ms = new ArrayList<>(); - for (Map<ByteBuffer, IMutation> ksMap : mutations.values()) - ms.addAll(ksMap.values()); - - return ms; - } - - private PartitionColumns updatedColumns() - { - return updatedColumns; + return collector.toMutations(); } private int updatedRows() @@ -272,55 +252,6 @@ public class BatchStatement implements CQLStatement return statements.size(); } - private void addStatementMutations(ModificationStatement statement, - QueryOptions options, - boolean local, - long now, - Map<String, Map<ByteBuffer, IMutation>> mutations) - throws RequestExecutionException, RequestValidationException - { - String ksName = statement.keyspace(); - Map<ByteBuffer, IMutation> ksMap = mutations.get(ksName); - if (ksMap == null) - { - ksMap = new HashMap<>(); - mutations.put(ksName, ksMap); - } - - // The following does the same than statement.getMutations(), but we inline it here because - // we don't want to recreate mutations every time as this is particularly inefficient when applying - // multiple batch to the same partition (see #6737). - List<ByteBuffer> keys = statement.buildPartitionKeyNames(options); - CBuilder clustering = statement.createClustering(options); - UpdateParameters params = statement.makeUpdateParameters(keys, clustering, options, local, now); - - for (ByteBuffer key : keys) - { - DecoratedKey dk = statement.cfm.decorateKey(key); - IMutation mutation = ksMap.get(dk.getKey()); - Mutation mut; - if (mutation == null) - { - mut = new Mutation(ksName, dk); - mutation = statement.cfm.isCounter() ? new CounterMutation(mut, options.getConsistency()) : mut; - ksMap.put(dk.getKey(), mutation); - } - else - { - mut = statement.cfm.isCounter() ? ((CounterMutation) mutation).getMutation() : (Mutation) mutation; - } - - PartitionUpdate upd = mut.get(statement.cfm); - if (upd == null) - { - upd = new PartitionUpdate(statement.cfm, dk, updatedColumns(), updatedRows()); - mut.add(upd); - } - - statement.addUpdateForKey(upd, clustering, params); - } - } - /** * Checks batch size to ensure threshold is met. If not, a warning is logged. * @@ -470,17 +401,23 @@ public class BatchStatement implements CQLStatement throw new InvalidRequestException("Batch with conditions cannot span multiple partitions"); } - CBuilder cbuilder = statement.createClustering(statementOptions); + SortedSet<Clustering> clusterings = statement.createClustering(statementOptions); + + checkFalse(clusterings.size() > 1, + "IN on the clustering key columns is not supported with conditional updates"); + + Clustering clustering = Iterables.getOnlyElement(clusterings); + if (statement.hasConditions()) { - statement.addConditions(cbuilder.build(), casRequest, statementOptions); + statement.addConditions(clustering, casRequest, statementOptions); // As soon as we have a ifNotExists, we set columnsWithConditions to null so that everything is in the resultSet if (statement.hasIfNotExistCondition() || statement.hasIfExistCondition()) columnsWithConditions = null; else if (columnsWithConditions != null) Iterables.addAll(columnsWithConditions, statement.getColumnsWithConditions()); } - casRequest.addRowUpdate(cbuilder, statement, statementOptions, timestamp); + casRequest.addRowUpdate(clustering, statement, statementOptions, timestamp); } return Pair.create(casRequest, columnsWithConditions); http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java index dc70bd2..1c3c795 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java @@ -67,9 +67,9 @@ public class CQL3CasRequest implements CASRequest this.updatesStaticRow = updatesStaticRow; } - public void addRowUpdate(CBuilder cbuilder, ModificationStatement stmt, QueryOptions options, long timestamp) + public void addRowUpdate(Clustering clustering, ModificationStatement stmt, QueryOptions options, long timestamp) { - updates.add(new RowUpdate(cbuilder, stmt, options, timestamp)); + updates.add(new RowUpdate(clustering, stmt, options, timestamp)); } public void addNotExist(Clustering clustering) throws InvalidRequestException @@ -129,7 +129,7 @@ public class CQL3CasRequest implements CASRequest return conditionColumns; } - public SinglePartitionReadCommand readCommand(int nowInSec) + public SinglePartitionReadCommand<?> readCommand(int nowInSec) { assert !conditions.isEmpty(); Slices.Builder builder = new Slices.Builder(cfm.comparator, conditions.size()); @@ -184,14 +184,14 @@ public class CQL3CasRequest implements CASRequest */ private class RowUpdate { - private final CBuilder cbuilder; + private final Clustering clustering; private final ModificationStatement stmt; private final QueryOptions options; private final long timestamp; - private RowUpdate(CBuilder cbuilder, ModificationStatement stmt, QueryOptions options, long timestamp) + private RowUpdate(Clustering clustering, ModificationStatement stmt, QueryOptions options, long timestamp) { - this.cbuilder = cbuilder; + this.clustering = clustering; this.stmt = stmt; this.options = options; this.timestamp = timestamp; @@ -201,7 +201,7 @@ public class CQL3CasRequest implements CASRequest { Map<DecoratedKey, Partition> map = stmt.requiresRead() ? Collections.<DecoratedKey, Partition>singletonMap(key, current) : null; UpdateParameters params = new UpdateParameters(cfm, updates.columns(), options, timestamp, stmt.getTimeToLive(options), map, true); - stmt.addUpdateForKey(updates, cbuilder, params); + stmt.addUpdateForKey(updates, clustering, params); } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/2e3727e3/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java index a33696e..cd6ce77 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -17,37 +17,38 @@ */ package org.apache.cassandra.cql3.statements; -import java.util.Iterator; import java.util.List; -import com.google.common.collect.Iterators; - import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.cql3.*; -import org.apache.cassandra.cql3.restrictions.Restriction; -import org.apache.cassandra.db.CBuilder; +import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.Slice; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.utils.Pair; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; + /** * A <code>DELETE</code> parsed from a CQL query statement. */ public class DeleteStatement extends ModificationStatement { - private DeleteStatement(StatementType type, int boundTerms, CFMetaData cfm, Attributes attrs) + private DeleteStatement(int boundTerms, + CFMetaData cfm, + Operations operations, + StatementRestrictions restrictions, + Conditions conditions, + Attributes attrs) { - super(type, boundTerms, cfm, attrs); + super(StatementType.DELETE, boundTerms, cfm, operations, restrictions, conditions, attrs); } - public boolean requireFullClusteringKey() - { - return false; - } - - public void addUpdateForKey(PartitionUpdate update, CBuilder cbuilder, UpdateParameters params) + @Override + public void addUpdateForKey(PartitionUpdate update, Clustering clustering, UpdateParameters params) throws InvalidRequestException { List<Operation> regularDeletions = getRegularOperations(); @@ -56,32 +57,29 @@ public class DeleteStatement extends ModificationStatement if (regularDeletions.isEmpty() && staticDeletions.isEmpty()) { // We're not deleting any specific columns so it's either a full partition deletion .... - if (cbuilder.count() == 0) + if (clustering.size() == 0) { update.addPartitionDeletion(params.deletionTime()); } // ... or a row deletion ... - else if (cbuilder.remainingCount() == 0) + else if (clustering.size() == cfm.clusteringColumns().size()) { - params.newRow(cbuilder.build()); + params.newRow(clustering); params.addRowDeletion(); update.add(params.buildRow()); } // ... or a range of rows deletion. else { - update.add(params.makeRangeTombstone(cbuilder)); + update.add(params.makeRangeTombstone(cfm.comparator, clustering)); } } else { if (!regularDeletions.isEmpty()) { - // We can't delete specific (regular) columns if not all clustering columns have been specified. - if (cbuilder.remainingCount() > 0) - throw new InvalidRequestException(String.format("Primary key column '%s' must be specified in order to delete column '%s'", getFirstEmptyKey().name, regularDeletions.get(0).column.name)); + params.newRow(clustering); - params.newRow(cbuilder.build()); for (Operation op : regularDeletions) op.execute(update.partitionKey(), params); update.add(params.buildRow()); @@ -99,21 +97,16 @@ public class DeleteStatement extends ModificationStatement params.validateIndexedColumns(update); } - protected void validateWhereClauseForConditions() throws InvalidRequestException + @Override + public void addUpdateForKey(PartitionUpdate update, Slice slice, UpdateParameters params) { - Iterator<ColumnDefinition> iterator = Iterators.concat(cfm.partitionKeyColumns().iterator(), cfm.clusteringColumns().iterator()); - while (iterator.hasNext()) - { - ColumnDefinition def = iterator.next(); - Restriction restriction = processedKeys.get(def.name); - if (restriction == null || !(restriction.isEQ() || restriction.isIN())) - { - throw new InvalidRequestException( - String.format("DELETE statements must restrict all PRIMARY KEY columns with equality relations in order " + - "to use IF conditions, but column '%s' is not restricted", def.name)); - } - } + List<Operation> regularDeletions = getRegularOperations(); + List<Operation> staticDeletions = getStaticOperations(); + + checkTrue(regularDeletions.isEmpty() && staticDeletions.isEmpty(), + "Range deletions are not supported for specific columns"); + update.add(params.makeRangeTombstone(slice)); } public static class Parsed extends ModificationStatement.Parsed @@ -133,28 +126,46 @@ public class DeleteStatement extends ModificationStatement this.whereClause = whereClause; } - protected ModificationStatement prepareInternal(CFMetaData cfm, VariableSpecifications boundNames, Attributes attrs) throws InvalidRequestException + + @Override + protected ModificationStatement prepareInternal(CFMetaData cfm, + VariableSpecifications boundNames, + Conditions conditions, + Attributes attrs) { - DeleteStatement stmt = new DeleteStatement(ModificationStatement.StatementType.DELETE, boundNames.size(), cfm, attrs); + Operations operations = new Operations(); for (Operation.RawDeletion deletion : deletions) { - ColumnIdentifier id = deletion.affectedColumn().prepare(cfm); - ColumnDefinition def = cfm.getColumnDefinition(id); - if (def == null) - throw new InvalidRequestException(String.format("Unknown identifier %s", id)); + ColumnDefinition def = getColumnDefinition(cfm, deletion.affectedColumn()); // For compact, we only have one value except the key, so the only form of DELETE that make sense is without a column // list. However, we support having the value name for coherence with the static/sparse case - if (def.isPrimaryKeyColumn()) - throw new InvalidRequestException(String.format("Invalid identifier %s for deletion (should not be a PRIMARY KEY part)", def.name)); + checkFalse(def.isPrimaryKeyColumn(), "Invalid identifier %s for deletion (should not be a PRIMARY KEY part)", def.name); Operation op = deletion.prepare(cfm.ksName, def); op.collectMarkerSpecification(boundNames); - stmt.addOperation(op); + operations.add(op); } - stmt.processWhereClause(whereClause, boundNames); + StatementRestrictions restrictions = newRestrictions(StatementType.DELETE, + cfm, + boundNames, + operations, + whereClause, + conditions); + + DeleteStatement stmt = new DeleteStatement(boundNames.size(), + cfm, + operations, + restrictions, + conditions, + attrs); + + if (stmt.hasConditions()) + checkTrue(restrictions.hasAllPKColumnsRestrictedByEqualities(), + "DELETE statements must restrict all PRIMARY KEY columns with equality relations" + + " in order to use IF conditions"); return stmt; } }