From 655ccc3abfc0f3fbe974d3f52cd38bf0d6afe2a9 Mon Sep 17 00:00:00 2001 From: Sylvain Lebresne Date: Mon, 16 Jan 2012 18:50:53 +0100 Subject: [PATCH] CQL 3.0 beta patch by slebresne; reviewed by jbellis and urandom for CASSANDRA-3761 --- CHANGES.txt | 1 + build.xml | 15 +- interface/cassandra.thrift | 6 +- src/avro/internode.genavro | 2 + .../apache/cassandra/config/CFMetaData.java | 91 +- .../apache/cassandra/cql/QueryProcessor.java | 3 +- .../org/apache/cassandra/cql3/Attributes.java | 32 + .../apache/cassandra/cql3/CFDefinition.java | 303 ++++++ .../org/apache/cassandra/cql3/CFName.java | 58 ++ .../org/apache/cassandra/cql3/CFPropDefs.java | 260 +++++ .../apache/cassandra/cql3/CQLStatement.java | 70 ++ .../cassandra/cql3/ColumnIdentifier.java | 79 ++ .../cassandra/cql3/ColumnNameBuilder.java | 73 ++ src/java/org/apache/cassandra/cql3/Cql.g | 633 ++++++++++++ .../org/apache/cassandra/cql3/Operation.java | 54 + .../apache/cassandra/cql3/QueryProcessor.java | 205 ++++ .../org/apache/cassandra/cql3/Relation.java | 117 +++ src/java/org/apache/cassandra/cql3/Term.java | 205 ++++ .../cql3/statements/AlterTableStatement.java | 180 ++++ .../cql3/statements/BatchStatement.java | 166 +++ .../cql3/statements/CFStatement.java | 56 + .../CreateColumnFamilyStatement.java | 294 ++++++ .../cql3/statements/CreateIndexStatement.java | 108 ++ .../statements/CreateKeyspaceStatement.java | 114 ++ .../cql3/statements/DeleteStatement.java | 164 +++ .../statements/DropColumnFamilyStatement.java | 39 + .../cql3/statements/DropIndexStatement.java | 74 ++ .../statements/DropKeyspaceStatement.java | 52 + .../statements/ModificationStatement.java | 129 +++ .../cql3/statements/Preprocessable.java | 27 + .../statements/SchemaAlteringStatement.java | 184 ++++ .../cql3/statements/SelectStatement.java | 972 ++++++++++++++++++ .../cql3/statements/TruncateStatement.java | 68 ++ .../cql3/statements/UpdateStatement.java | 359 +++++++ .../cql3/statements/UseStatement.java | 48 + .../apache/cassandra/db/CounterMutation.java | 9 + .../org/apache/cassandra/db/IMutation.java | 1 + .../org/apache/cassandra/db/RowMutation.java | 19 + .../org/apache/cassandra/db/SystemTable.java | 2 +- .../db/marshal/AbstractCompositeType.java | 17 + .../cassandra/db/marshal/CompositeType.java | 128 ++- .../apache/cassandra/service/ClientState.java | 57 +- .../cassandra/service/StorageService.java | 1 + .../cassandra/thrift/CassandraServer.java | 42 +- .../cassandra/thrift/ThriftValidation.java | 8 +- .../cassandra/utils/SemanticVersion.java | 231 +++++ .../cassandra/utils/SemanticVersionTest.java | 105 ++ 47 files changed, 5829 insertions(+), 32 deletions(-) create mode 100644 src/java/org/apache/cassandra/cql3/Attributes.java create mode 100644 src/java/org/apache/cassandra/cql3/CFDefinition.java create mode 100644 src/java/org/apache/cassandra/cql3/CFName.java create mode 100644 src/java/org/apache/cassandra/cql3/CFPropDefs.java create mode 100644 src/java/org/apache/cassandra/cql3/CQLStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/ColumnIdentifier.java create mode 100644 src/java/org/apache/cassandra/cql3/ColumnNameBuilder.java create mode 100644 src/java/org/apache/cassandra/cql3/Cql.g create mode 100644 src/java/org/apache/cassandra/cql3/Operation.java create mode 100644 src/java/org/apache/cassandra/cql3/QueryProcessor.java create mode 100644 src/java/org/apache/cassandra/cql3/Relation.java create mode 100644 src/java/org/apache/cassandra/cql3/Term.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/BatchStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/CFStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/DropColumnFamilyStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/Preprocessable.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/SelectStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java create mode 100644 src/java/org/apache/cassandra/cql3/statements/UseStatement.java create mode 100644 src/java/org/apache/cassandra/utils/SemanticVersion.java create mode 100644 test/unit/org/apache/cassandra/utils/SemanticVersionTest.java diff --git a/CHANGES.txt b/CHANGES.txt index 66ab562ab2..03d2e699a7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -55,6 +55,7 @@ * Add SnapshotCommand to trigger snapshot on remote node (CASSANDRA-3721) * Make CFMetaData conversions to/from thrift/native schema inverses (CASSANDRA_3559) + * Add initial code for CQL 3.0-beta (CASSANDRA-3781) 1.0.8 diff --git a/build.xml b/build.xml index d382e340dd..2a46f37fae 100644 --- a/build.xml +++ b/build.xml @@ -202,6 +202,9 @@ + @@ -213,9 +216,17 @@ - + + + + + + - + diff --git a/interface/cassandra.thrift b/interface/cassandra.thrift index e28b236a23..08b776edcb 100644 --- a/interface/cassandra.thrift +++ b/interface/cassandra.thrift @@ -46,7 +46,7 @@ namespace rb CassandraThrift # for every edit that doesn't result in a change to major/minor. # # See the Semantic Versioning Specification (SemVer) http://semver.org. -const string VERSION = "19.25.0" +const string VERSION = "19.26.0" # @@ -415,6 +415,8 @@ struct CfDef { 32: optional map compression_options, 33: optional double bloom_filter_fp_chance, 34: optional string caching="keys_only", + 35: optional list column_aliases, + 36: optional binary value_alias, } /* describes a keyspace. */ @@ -714,6 +716,6 @@ service Cassandra { 2:UnavailableException ue, 3:TimedOutException te, 4:SchemaDisagreementException sde) - + void set_cql_version(1: required string version) throws (1:InvalidRequestException ire) } diff --git a/src/avro/internode.genavro b/src/avro/internode.genavro index c36a83f20b..36c2cba400 100644 --- a/src/avro/internode.genavro +++ b/src/avro/internode.genavro @@ -67,6 +67,8 @@ protocol InterNode { union { null, map } compression_options = null; union { null, double } bloom_filter_fp_chance = null; union { null, string } caching = null; + union { null, array } column_aliases = null; + union { null, bytes } value_alias = null; } @aliases(["org.apache.cassandra.config.avro.KsDef"]) diff --git a/src/java/org/apache/cassandra/config/CFMetaData.java b/src/java/org/apache/cassandra/config/CFMetaData.java index 9bd731a002..a82639d91d 100644 --- a/src/java/org/apache/cassandra/config/CFMetaData.java +++ b/src/java/org/apache/cassandra/config/CFMetaData.java @@ -32,6 +32,7 @@ import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.cassandra.cql3.CFDefinition; import org.apache.cassandra.db.*; import org.apache.cassandra.db.filter.QueryPath; import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; @@ -173,6 +174,8 @@ public final class CFMetaData // thrift compatibility private double mergeShardsChance; // default 0.1, chance [0.0, 1.0] of merging old shards during replication private ByteBuffer keyAlias; // default NULL + private List columnAliases = new ArrayList(); + private ByteBuffer valueAlias; // default NULL private Double bloomFilterFpChance; // default NULL private Caching caching; // default KEYS_ONLY (possible: all, key_only, row_only, none) @@ -182,17 +185,24 @@ public final class CFMetaData private CompressionParameters compressionParameters; + // Processed infos used by CQL. This can be fully reconstructed from the CFMedata, + // so it's not saved on disk. It is however costlyish to recreate for each query + // so we cache it here (and update on each relevant CFMetadata change) + private CFDefinition cqlCfDef; + public CFMetaData comment(String prop) { comment = enforceCommentNotNull(prop); return this;} public CFMetaData readRepairChance(double prop) {readRepairChance = prop; return this;} public CFMetaData replicateOnWrite(boolean prop) {replicateOnWrite = prop; return this;} public CFMetaData gcGraceSeconds(int prop) {gcGraceSeconds = prop; return this;} - public CFMetaData defaultValidator(AbstractType prop) {defaultValidator = prop; return this;} - public CFMetaData keyValidator(AbstractType prop) {keyValidator = prop; return this;} + public CFMetaData defaultValidator(AbstractType prop) {defaultValidator = prop; updateCfDef(); return this;} + public CFMetaData keyValidator(AbstractType prop) {keyValidator = prop; updateCfDef(); return this;} public CFMetaData minCompactionThreshold(int prop) {minCompactionThreshold = prop; return this;} public CFMetaData maxCompactionThreshold(int prop) {maxCompactionThreshold = prop; return this;} public CFMetaData mergeShardsChance(double prop) {mergeShardsChance = prop; return this;} - public CFMetaData keyAlias(ByteBuffer prop) {keyAlias = prop; return this;} - public CFMetaData columnMetadata(Map prop) {column_metadata = prop; return this;} + public CFMetaData keyAlias(ByteBuffer prop) {keyAlias = prop; updateCfDef(); return this;} + public CFMetaData columnAliases(List prop) {columnAliases = prop; updateCfDef(); return this;} + public CFMetaData valueAlias(ByteBuffer prop) {valueAlias = prop; updateCfDef(); return this;} + public CFMetaData columnMetadata(Map prop) {column_metadata = prop; updateCfDef(); return this;} public CFMetaData compactionStrategyClass(Class prop) {compactionStrategyClass = prop; return this;} public CFMetaData compactionStrategyOptions(Map prop) {compactionStrategyOptions = prop; return this;} public CFMetaData compressionParameters(CompressionParameters prop) {compressionParameters = prop; return this;} @@ -246,6 +256,7 @@ public final class CFMetaData keyValidator = BytesType.instance; comment = ""; keyAlias = null; // This qualifies as a 'strange default'. + valueAlias = null; column_metadata = new HashMap(); try @@ -259,6 +270,7 @@ public final class CFMetaData compactionStrategyOptions = new HashMap(); compressionParameters = new CompressionParameters(null); + updateCfDef(); // init cqlCfDef } private static CFMetaData newSystemMetadata(String cfName, int cfId, String comment, AbstractType comparator, AbstractType subcc) @@ -369,6 +381,8 @@ public final class CFMetaData if (cf.max_compaction_threshold != null) { newCFMD.maxCompactionThreshold(cf.max_compaction_threshold); } if (cf.merge_shards_chance != null) { newCFMD.mergeShardsChance(cf.merge_shards_chance); } if (cf.key_alias != null) { newCFMD.keyAlias(cf.key_alias); } + if (cf.column_aliases != null) { newCFMD.columnAliases(fixAvroRetardation(cf.column_aliases)); } + if (cf.value_alias != null) { newCFMD.valueAlias(cf.value_alias); } if (cf.compaction_strategy != null) { try @@ -418,6 +432,18 @@ public final class CFMetaData .bloomFilterFpChance(cf.bloom_filter_fp_chance) .caching(caching); } + + /* + * Avro handles array with it's own class, GenericArray, that extends + * AbstractList but redefine equals() in a way that violate List.equals() + * specification (basically only a GenericArray can ever be equal to a + * GenericArray). + * (Concretely, keeping the list returned by avro breaks DefsTest.saveAndRestore()) + */ + private static List fixAvroRetardation(List array) + { + return new ArrayList(array); + } public String getComment() { @@ -469,6 +495,21 @@ public final class CFMetaData return keyAlias == null ? DEFAULT_KEY_NAME : keyAlias; } + public ByteBuffer getKeyAlias() + { + return keyAlias; + } + + public List getColumnAliases() + { + return columnAliases; + } + + public ByteBuffer getValueAlias() + { + return valueAlias; + } + public CompressionParameters compressionParameters() { return compressionParameters; @@ -524,6 +565,8 @@ public final class CFMetaData .append(column_metadata, rhs.column_metadata) .append(mergeShardsChance, rhs.mergeShardsChance) .append(keyAlias, rhs.keyAlias) + .append(columnAliases, rhs.columnAliases) + .append(valueAlias, rhs.valueAlias) .append(compactionStrategyClass, rhs.compactionStrategyClass) .append(compactionStrategyOptions, rhs.compactionStrategyOptions) .append(compressionParameters, rhs.compressionParameters) @@ -552,6 +595,8 @@ public final class CFMetaData .append(column_metadata) .append(mergeShardsChance) .append(keyAlias) + .append(columnAliases) + .append(valueAlias) .append(compactionStrategyClass) .append(compactionStrategyOptions) .append(compressionParameters) @@ -613,6 +658,8 @@ public final class CFMetaData if (cf_def.isSetMax_compaction_threshold()) { newCFMD.maxCompactionThreshold(cf_def.max_compaction_threshold); } if (cf_def.isSetMerge_shards_chance()) { newCFMD.mergeShardsChance(cf_def.merge_shards_chance); } if (cf_def.isSetKey_alias()) { newCFMD.keyAlias(cf_def.key_alias); } + if (cf_def.isSetColumn_aliases() && cf_def.column_aliases != null) { newCFMD.columnAliases(cf_def.column_aliases); } + if (cf_def.isSetValue_alias()) { newCFMD.valueAlias(cf_def.value_alias); } if (cf_def.isSetKey_validation_class()) { newCFMD.keyValidator(TypeParser.parse(cf_def.key_validation_class)); } if (cf_def.isSetCompaction_strategy()) newCFMD.compactionStrategyClass = createCompactionStrategy(cf_def.compaction_strategy); @@ -698,6 +745,8 @@ public final class CFMetaData maxCompactionThreshold = cf_def.max_compaction_threshold; mergeShardsChance = cf_def.merge_shards_chance; keyAlias = cf_def.key_alias; + columnAliases = cf_def.column_aliases; + valueAlias = cf_def.value_alias; if (cf_def.isSetBloom_filter_fp_chance()) bloomFilterFpChance = cf_def.bloom_filter_fp_chance; caching = Caching.fromString(cf_def.caching); @@ -1073,14 +1122,29 @@ public final class CFMetaData if (!cfDef.isSetColumn_metadata()) return; - AbstractType comparator = TypeParser.parse(cfDef.column_type.equals("Super") - ? cfDef.subcomparator_type - : cfDef.comparator_type); + AbstractType comparator = getColumnDefinitionComparator(cfDef); for (ColumnDef columnDef : cfDef.column_metadata) ColumnDefinition.addToSchema(mutation, cfDef.name, comparator, columnDef, timestamp); } + public static AbstractType getColumnDefinitionComparator(CfDef cfDef) throws ConfigurationException + { + AbstractType cfComparator = TypeParser.parse(cfDef.column_type.equals("Super") + ? cfDef.subcomparator_type + : cfDef.comparator_type); + + if (cfComparator instanceof CompositeType) + { + List> types = ((CompositeType)cfComparator).types; + return types.get(types.size() - 1); + } + else + { + return cfComparator; + } + } + /** * Deserialize CF metadata from low-level representation * @@ -1130,6 +1194,17 @@ public final class CFMetaData return cfDef; } + private void updateCfDef() + { + cqlCfDef = new CFDefinition(this); + } + + public CFDefinition getCfDef() + { + assert cqlCfDef != null; + return cqlCfDef; + } + @Override public String toString() { @@ -1150,6 +1225,8 @@ public final class CFMetaData .append("maxCompactionThreshold", maxCompactionThreshold) .append("mergeShardsChance", mergeShardsChance) .append("keyAlias", keyAlias) + .append("columnAliases", columnAliases) + .append("valueAlias", keyAlias) .append("column_metadata", column_metadata) .append("compactionStrategyClass", compactionStrategyClass) .append("compactionStrategyOptions", compactionStrategyOptions) diff --git a/src/java/org/apache/cassandra/cql/QueryProcessor.java b/src/java/org/apache/cassandra/cql/QueryProcessor.java index 917fb92f13..c539979794 100644 --- a/src/java/org/apache/cassandra/cql/QueryProcessor.java +++ b/src/java/org/apache/cassandra/cql/QueryProcessor.java @@ -53,6 +53,7 @@ import org.apache.cassandra.thrift.Column; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.SemanticVersion; import com.google.common.base.Predicates; import com.google.common.collect.Maps; @@ -65,7 +66,7 @@ import static org.apache.cassandra.thrift.ThriftValidation.validateColumnFamily; public class QueryProcessor { - public static final String CQL_VERSION = "2.0.0"; + public static final SemanticVersion CQL_VERSION = new SemanticVersion("2.0.0"); private static final Logger logger = LoggerFactory.getLogger(QueryProcessor.class); diff --git a/src/java/org/apache/cassandra/cql3/Attributes.java b/src/java/org/apache/cassandra/cql3/Attributes.java new file mode 100644 index 0000000000..41f78fb498 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Attributes.java @@ -0,0 +1,32 @@ +/* + * 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.thrift.ConsistencyLevel; + +/** + * Utility class for the Parser to gather attributes for modification + * statements. + */ +public class Attributes +{ + public ConsistencyLevel cLevel; + public Long timestamp; + public int timeToLive; +} diff --git a/src/java/org/apache/cassandra/cql3/CFDefinition.java b/src/java/org/apache/cassandra/cql3/CFDefinition.java new file mode 100644 index 0000000000..b3725db6d7 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/CFDefinition.java @@ -0,0 +1,303 @@ +/* + * 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.nio.ByteBuffer; +import java.util.*; + +import com.google.common.collect.AbstractIterator; + +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * Holds metadata on a CF preprocessed for use by CQL queries. + */ +public class CFDefinition implements Iterable +{ + public static final AbstractType definitionType = UTF8Type.instance; + + private static final String DEFAULT_KEY_ALIAS = "key"; + private static final String DEFAULT_COLUMN_ALIAS = "column"; + private static final String DEFAULT_VALUE_ALIAS = "value"; + + public final CFMetaData cfm; + public final Name key; + // LinkedHashMap because the order does matter (it is the order in the composite type) + public final LinkedHashMap columns = new LinkedHashMap(); + public final Name value; + // Keep metadata lexicographically ordered so that wildcard expansion have a deterministic order + public final Map metadata = new TreeMap(); + + public final boolean isComposite; + public final boolean isCompact; + + public CFDefinition(CFMetaData cfm) + { + this.cfm = cfm; + this.key = new Name(getKeyId(cfm), Name.Kind.KEY_ALIAS, cfm.getKeyValidator()); + if (cfm.comparator instanceof CompositeType) + { + this.isComposite = true; + CompositeType composite = (CompositeType)cfm.comparator; + if (!cfm.getColumn_metadata().isEmpty()) + { + // "sparse" composite + this.isCompact = false; + this.value = null; + assert cfm.getValueAlias() == null; + for (int i = 0; i < composite.types.size() - 1; i++) + { + ColumnIdentifier id = getColumnId(cfm, i); + this.columns.put(id, new Name(id, Name.Kind.COLUMN_ALIAS, i, composite.types.get(i))); + } + + for (Map.Entry def : cfm.getColumn_metadata().entrySet()) + { + ColumnIdentifier id = new ColumnIdentifier(def.getKey()); + this.metadata.put(id, new Name(id, Name.Kind.COLUMN_METADATA, def.getValue().getValidator())); + } + } + else + { + // "dense" composite + this.isCompact = true; + for (int i = 0; i < composite.types.size(); i++) + { + ColumnIdentifier id = getColumnId(cfm, i); + this.columns.put(id, new Name(id, Name.Kind.COLUMN_ALIAS, i, composite.types.get(i))); + } + this.value = new Name(getValueId(cfm), Name.Kind.VALUE_ALIAS, cfm.getDefaultValidator()); + } + } + else + { + this.isComposite = false; + if (!cfm.getColumn_metadata().isEmpty()) + { + // static CF + this.isCompact = false; + this.value = null; + assert cfm.getValueAlias() == null; + assert cfm.getColumnAliases() == null || cfm.getColumnAliases().isEmpty(); + for (Map.Entry def : cfm.getColumn_metadata().entrySet()) + { + ColumnIdentifier id = new ColumnIdentifier(def.getKey()); + this.metadata.put(id, new Name(id, Name.Kind.COLUMN_METADATA, def.getValue().getValidator())); + } + } + else + { + // dynamic CF + this.isCompact = true; + ColumnIdentifier id = getColumnId(cfm, 0); + Name name = new Name(id, Name.Kind.COLUMN_ALIAS, 0, cfm.comparator); + this.columns.put(id, name); + this.value = new Name(getValueId(cfm), Name.Kind.VALUE_ALIAS, cfm.getDefaultValidator()); + } + } + assert value == null || metadata.isEmpty(); + } + + private static ColumnIdentifier getKeyId(CFMetaData cfm) + { + return cfm.getKeyAlias() == null + ? new ColumnIdentifier(DEFAULT_KEY_ALIAS, false) + : new ColumnIdentifier(cfm.getKeyAlias()); + } + + private static ColumnIdentifier getColumnId(CFMetaData cfm, int i) + { + List definedNames = cfm.getColumnAliases(); + return definedNames == null || i >= definedNames.size() + ? new ColumnIdentifier(DEFAULT_COLUMN_ALIAS + (i + 1), false) + : new ColumnIdentifier(cfm.getColumnAliases().get(i)); + } + + private static ColumnIdentifier getValueId(CFMetaData cfm) + { + return cfm.getValueAlias() == null + ? new ColumnIdentifier(DEFAULT_VALUE_ALIAS, false) + : new ColumnIdentifier(cfm.getValueAlias()); + } + + public Name get(ColumnIdentifier name) + { + if (name.equals(key.name)) + return key; + if (value != null && name.equals(value.name)) + return value; + CFDefinition.Name def = columns.get(name); + if (def != null) + return def; + return metadata.get(name); + } + + public Iterator iterator() + { + return new AbstractIterator() + { + private boolean keyDone; + private final Iterator columnIter = columns.values().iterator(); + private boolean valueDone; + private final Iterator metadataIter = metadata.values().iterator(); + + protected Name computeNext() + { + if (!keyDone) + { + keyDone = true; + return key; + } + + if (columnIter.hasNext()) + return columnIter.next(); + + if (value != null && !valueDone) + { + valueDone = true; + return value; + } + + if (metadataIter.hasNext()) + return metadataIter.next(); + + return endOfData(); + } + }; + } + + public ColumnNameBuilder getColumnNameBuilder() + { + return isComposite + ? new CompositeType.Builder((CompositeType)cfm.comparator) + : new NonCompositeBuilder(cfm.comparator); + } + + public static class Name + { + public static enum Kind + { + KEY_ALIAS, COLUMN_ALIAS, VALUE_ALIAS, COLUMN_METADATA + } + + private Name(ColumnIdentifier name, Kind kind, AbstractType type) + { + this(name, kind, -1, type); + } + + private Name(ColumnIdentifier name, Kind kind, int position, AbstractType type) + { + this.kind = kind; + this.name = name; + this.compositePosition = position; + this.type = type; + } + + public final Kind kind; + public final ColumnIdentifier name; + public final int compositePosition; // only make sense for COLUMN_ALIAS if CFDefinition.isComposite() + public final AbstractType type; + + @Override + public String toString() + { + // It is not fully conventional, but it is convenient for error messages to the user + return name.toString(); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(key.name); + for (Name name : columns.values()) + sb.append(", ").append(name.name); + sb.append(" => "); + if (value != null) + sb.append(value.name); + if (!metadata.isEmpty()) + { + sb.append("{"); + for (Name name : metadata.values()) + sb.append(" ").append(name.name); + sb.append(" }"); + } + sb.append("]"); + return sb.toString(); + } + + private static class NonCompositeBuilder implements ColumnNameBuilder + { + private final AbstractType type; + private ByteBuffer columnName; + + private NonCompositeBuilder(AbstractType type) + { + this.type = type; + } + + public NonCompositeBuilder add(ByteBuffer bb) + { + if (columnName != null) + throw new IllegalStateException("Column name is already constructed"); + + columnName = bb; + return this; + } + + public NonCompositeBuilder add(Term t, Relation.Type op, List variables) throws InvalidRequestException + { + if (columnName != null) + throw new IllegalStateException("Column name is already constructed"); + + // We don't support the relation type yet, i.e., there is no distinction between x > 3 and x >= 3. + columnName = t.getByteBuffer(type, variables); + return this; + } + + public int componentCount() + { + return columnName == null ? 0 : 1; + } + + public ByteBuffer build() + { + return columnName == null ? ByteBufferUtil.EMPTY_BYTE_BUFFER : columnName; + } + + public ByteBuffer buildAsEndOfRange() + { + throw new IllegalStateException(); + } + + public NonCompositeBuilder copy() + { + NonCompositeBuilder newBuilder = new NonCompositeBuilder(type); + newBuilder.columnName = columnName; + return newBuilder; + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/CFName.java b/src/java/org/apache/cassandra/cql3/CFName.java new file mode 100644 index 0000000000..cd6e6d3452 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/CFName.java @@ -0,0 +1,58 @@ +/* + * 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.Locale; + +public class CFName +{ + private String ksName; + private String cfName; + + public void setKeyspace(String ks, boolean keepCase) + { + ksName = keepCase ? ks : ks.toLowerCase(Locale.US); + } + + public void setColumnFamily(String cf, boolean keepCase) + { + cfName = keepCase ? cf : cf.toLowerCase(Locale.US); + } + + public boolean hasKeyspace() + { + return ksName != null; + } + + public String getKeyspace() + { + return ksName; + } + + public String getColumnFamily() + { + return cfName; + } + + @Override + public String toString() + { + return (hasKeyspace() ? (ksName + ".") : "") + cfName; + } +} diff --git a/src/java/org/apache/cassandra/cql3/CFPropDefs.java b/src/java/org/apache/cassandra/cql3/CFPropDefs.java new file mode 100644 index 0000000000..e878e63451 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/CFPropDefs.java @@ -0,0 +1,260 @@ +/* + * 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 com.google.common.collect.Sets; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.TypeParser; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class CFPropDefs +{ + private static Logger logger = LoggerFactory.getLogger(CFPropDefs.class); + + public static final String KW_COMMENT = "comment"; + public static final String KW_READREPAIRCHANCE = "read_repair_chance"; + public static final String KW_GCGRACESECONDS = "gc_grace_seconds"; + public static final String KW_MINCOMPACTIONTHRESHOLD = "min_compaction_threshold"; + public static final String KW_MAXCOMPACTIONTHRESHOLD = "max_compaction_threshold"; + public static final String KW_REPLICATEONWRITE = "replicate_on_write"; + + public static final String KW_COMPACTION_STRATEGY_CLASS = "compaction_strategy_class"; + + // Maps CQL short names to the respective Cassandra comparator/validator class names + public static final Map comparators = new HashMap(); + public static final Set keywords = new HashSet(); + public static final Set obsoleteKeywords = new HashSet(); + public static final Set allowedKeywords = new HashSet(); + + public static final String COMPACTION_OPTIONS_PREFIX = "compaction_strategy_options"; + public static final String COMPRESSION_PARAMETERS_PREFIX = "compression_parameters"; + + static + { + comparators.put("ascii", "AsciiType"); + comparators.put("bigint", "LongType"); + comparators.put("blob", "BytesType"); + comparators.put("boolean", "BooleanType"); + comparators.put("counter", "CounterColumnType"); + comparators.put("decimal", "DecimalType"); + comparators.put("double", "DoubleType"); + comparators.put("float", "FloatType"); + comparators.put("int", "Int32Type"); + comparators.put("text", "UTF8Type"); + comparators.put("timestamp", "DateType"); + comparators.put("uuid", "UUIDType"); + comparators.put("varchar", "UTF8Type"); + comparators.put("varint", "IntegerType"); + + keywords.add(KW_COMMENT); + keywords.add(KW_READREPAIRCHANCE); + keywords.add(KW_GCGRACESECONDS); + keywords.add(KW_MINCOMPACTIONTHRESHOLD); + keywords.add(KW_MAXCOMPACTIONTHRESHOLD); + keywords.add(KW_REPLICATEONWRITE); + keywords.add(KW_COMPACTION_STRATEGY_CLASS); + + obsoleteKeywords.add("row_cache_size"); + obsoleteKeywords.add("key_cache_size"); + obsoleteKeywords.add("row_cache_save_period_in_seconds"); + obsoleteKeywords.add("key_cache_save_period_in_seconds"); + obsoleteKeywords.add("memtable_throughput_in_mb"); + obsoleteKeywords.add("memtable_operations_in_millions"); + obsoleteKeywords.add("memtable_flush_after_mins"); + obsoleteKeywords.add("row_cache_provider"); + obsoleteKeywords.add("comparator"); + obsoleteKeywords.add("default_validation"); + + allowedKeywords.addAll(keywords); + allowedKeywords.addAll(obsoleteKeywords); + } + + public final Map properties = new HashMap(); + public final Map compactionStrategyOptions = new HashMap(); + public final Map compressionParameters = new HashMap(); + + public static AbstractType parseType(String type) throws InvalidRequestException + { + try + { + String className = comparators.get(type); + if (className == null) + className = type; + return TypeParser.parse(className); + } + catch (ConfigurationException e) + { + InvalidRequestException ex = new InvalidRequestException(e.toString()); + ex.initCause(e); + throw ex; + } + } + + /* If not comparator/validator is not specified, default to text (BytesType is the wrong default for CQL + * since it uses hex terms). If the value specified is not found in the comparators map, assume the user + * knows what they are doing (a custom comparator/validator for example), and pass it on as-is. + */ + + public void validate() throws InvalidRequestException + { + // Catch the case where someone passed a kwarg that is not recognized. + for (String bogus : Sets.difference(properties.keySet(), allowedKeywords)) + throw new InvalidRequestException(bogus + " is not a valid keyword argument for CREATE COLUMNFAMILY"); + for (String obsolete : Sets.intersection(properties.keySet(), obsoleteKeywords)) + logger.warn("Ignoring obsolete property {}", obsolete); + + // Validate min/max compaction thresholds + Integer minCompaction = getInt(KW_MINCOMPACTIONTHRESHOLD, null); + Integer maxCompaction = getInt(KW_MAXCOMPACTIONTHRESHOLD, null); + + if ((minCompaction != null) && (maxCompaction != null)) // Both min and max are set + { + if ((minCompaction > maxCompaction) && (maxCompaction != 0)) + throw new InvalidRequestException(String.format("%s cannot be larger than %s", + KW_MINCOMPACTIONTHRESHOLD, + KW_MAXCOMPACTIONTHRESHOLD)); + } + else if (minCompaction != null) // Only the min threshold is set + { + if (minCompaction > CFMetaData.DEFAULT_MAX_COMPACTION_THRESHOLD) + throw new InvalidRequestException(String.format("%s cannot be larger than %s, (default %s)", + KW_MINCOMPACTIONTHRESHOLD, + KW_MAXCOMPACTIONTHRESHOLD, + CFMetaData.DEFAULT_MAX_COMPACTION_THRESHOLD)); + } + else if (maxCompaction != null) // Only the max threshold is set + { + if ((maxCompaction < CFMetaData.DEFAULT_MIN_COMPACTION_THRESHOLD) && (maxCompaction != 0)) + throw new InvalidRequestException(String.format("%s cannot be smaller than %s, (default %s)", + KW_MAXCOMPACTIONTHRESHOLD, + KW_MINCOMPACTIONTHRESHOLD, + CFMetaData.DEFAULT_MIN_COMPACTION_THRESHOLD)); + } + } + + /** Map a keyword to the corresponding value */ + public void addProperty(String name, String value) + { + String[] composite = name.split(":"); + if (composite.length > 1) + { + if (composite[0].equals(COMPACTION_OPTIONS_PREFIX)) + { + compactionStrategyOptions.put(composite[1], value); + return; + } + else if (composite[0].equals(COMPRESSION_PARAMETERS_PREFIX)) + { + compressionParameters.put(composite[1], value); + return; + } + } + properties.put(name, value); + } + + public void addAll(Map propertyMap) + { + for (Map.Entry entry : propertyMap.entrySet()) + addProperty(entry.getKey(), entry.getValue()); + } + + public Boolean hasProperty(String name) + { + return properties.containsKey(name); + } + + public String get(String name) + { + return properties.get(name); + } + + public String getString(String key, String defaultValue) + { + String value = properties.get(key); + return value != null ? value : defaultValue; + } + + // Return a property value, typed as a Boolean + public Boolean getBoolean(String key, Boolean defaultValue) throws InvalidRequestException + { + String value = properties.get(key); + return (value == null) ? defaultValue : value.toLowerCase().matches("(1|true|yes)"); + } + + // Return a property value, typed as a Double + public Double getDouble(String key, Double defaultValue) throws InvalidRequestException + { + Double result; + String value = properties.get(key); + + if (value == null) + result = defaultValue; + else + { + try + { + result = Double.parseDouble(value); + } + catch (NumberFormatException e) + { + throw new InvalidRequestException(String.format("%s not valid for \"%s\"", value, key)); + } + } + return result; + } + + // Return a property value, typed as an Integer + public Integer getInt(String key, Integer defaultValue) throws InvalidRequestException + { + Integer result; + String value = properties.get(key); + + if (value == null) + result = defaultValue; + else + { + try + { + result = Integer.parseInt(value); + } + catch (NumberFormatException e) + { + throw new InvalidRequestException(String.format("%s not valid for \"%s\"", value, key)); + } + } + return result; + } + + public String toString() + { + return String.format("CFPropDefs(%s, compaction: %s, compression: %s)", + properties.toString(), + compactionStrategyOptions.toString(), + compressionParameters.toString()); + } +} diff --git a/src/java/org/apache/cassandra/cql3/CQLStatement.java b/src/java/org/apache/cassandra/cql3/CQLStatement.java new file mode 100644 index 0000000000..90277c0db3 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/CQLStatement.java @@ -0,0 +1,70 @@ +/* + * 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.nio.ByteBuffer; +import java.util.List; + +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.SchemaDisagreementException; +import org.apache.cassandra.thrift.TimedOutException; +import org.apache.cassandra.thrift.UnavailableException; + +public abstract class CQLStatement +{ + private int boundTerms; + + public int getBoundsTerms() + { + return boundTerms; + } + + // Used by the parser and preparable statement + public void setBoundTerms(int boundTerms) + { + this.boundTerms = boundTerms; + } + + /** + * Perform any access verification necessary for the statement. + * + * @param state the current client state + */ + public abstract void checkAccess(ClientState state) throws InvalidRequestException; + + /** + * Perform additional validation required by the statment. + * To be overriden by subclasses if needed. + * + * @param state the current client state + */ + public void validate(ClientState state) throws InvalidRequestException, SchemaDisagreementException + {} + + /** + * Execute the statement and return the resulting result or null if there is no result. + * + * @param state the current client state + * @param variables the values for bounded variables. The implementation + * can assume that each bound term have a corresponding value. + */ + public abstract CqlResult execute(ClientState state, List variables) throws InvalidRequestException, UnavailableException, TimedOutException, SchemaDisagreementException; +} diff --git a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java new file mode 100644 index 0000000000..60f3ecb55b --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java @@ -0,0 +1,79 @@ +/* + * 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.Locale; +import java.nio.charset.CharacterCodingException; +import java.nio.ByteBuffer; + +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * Represents an identifer for a CQL column definition. + */ +public class ColumnIdentifier implements Comparable +{ + public final ByteBuffer key; + private final String text; + + public ColumnIdentifier(String rawText, boolean keepCase) + { + this.text = keepCase ? rawText : rawText.toLowerCase(Locale.US); + this.key = ByteBufferUtil.bytes(this.text); + } + + public ColumnIdentifier(ByteBuffer key) + { + try + { + this.key = key; + this.text = ByteBufferUtil.string(key); + } + catch (CharacterCodingException e) + { + throw new RuntimeException(e); + } + } + + @Override + public final int hashCode() + { + return key.hashCode(); + } + + @Override + public final boolean equals(Object o) + { + if(!(o instanceof ColumnIdentifier)) + return false; + ColumnIdentifier that = (ColumnIdentifier)o; + return key.equals(that.key); + } + + @Override + public String toString() + { + return text; + } + + public int compareTo(ColumnIdentifier other) + { + return key.compareTo(other.key); + } +} diff --git a/src/java/org/apache/cassandra/cql3/ColumnNameBuilder.java b/src/java/org/apache/cassandra/cql3/ColumnNameBuilder.java new file mode 100644 index 0000000000..d9ca9023a0 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/ColumnNameBuilder.java @@ -0,0 +1,73 @@ +/* + * 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.nio.ByteBuffer; +import java.util.List; + +import org.apache.cassandra.thrift.InvalidRequestException; + +/** + * Build a potentially composite column name. + */ +public interface ColumnNameBuilder +{ + /** + * Add a new ByteBuffer as the next component for this name. + * @param bb the ByteBuffer to add + * @throws IllegalStateException if the builder if full, i.e. if enough component has been added. + * @return this builder + */ + public ColumnNameBuilder add(ByteBuffer bb); + + /** + * Add a new Term as the next component for this name. + * @param t the Term to add + * @param op the relationship this component should respect. + * @param variables the variables corresponding to prepared markers + * @throws IllegalStateException if the builder if full, i.e. if enough component has been added. + * @return this builder + */ + public ColumnNameBuilder add(Term t, Relation.Type op, List variables) throws InvalidRequestException; + + /** + * Returns the number of component already added to this builder. + * @return the number of component in this Builder + */ + public int componentCount(); + + /** + * Build the column name. + * @return the built column name + */ + public ByteBuffer build(); + + /** + * Build the column name so that the result sorts at the end of the range + * represented by this (uncomplete) column name. + * @throws IllegalStateException if the builder is empty or full. + */ + public ByteBuffer buildAsEndOfRange(); + + /** + * Clone this builder. + * @return the cloned builder. + */ + public ColumnNameBuilder copy(); +} diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g new file mode 100644 index 0000000000..13b2956d28 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Cql.g @@ -0,0 +1,633 @@ +/* + * 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. + */ + +grammar Cql; + +options { + language = Java; +} + +@header { + package org.apache.cassandra.cql3; + + import java.util.Map; + import java.util.HashMap; + import java.util.Collections; + import java.util.List; + import java.util.ArrayList; + + import org.apache.cassandra.cql3.statements.*; + import org.apache.cassandra.utils.Pair; + import org.apache.cassandra.thrift.ConsistencyLevel; + import org.apache.cassandra.thrift.InvalidRequestException; +} + +@members { + private List recognitionErrors = new ArrayList(); + private int currentBindMarkerIdx = -1; + + public void displayRecognitionError(String[] tokenNames, RecognitionException e) + { + String hdr = getErrorHeader(e); + String msg = getErrorMessage(e, tokenNames); + recognitionErrors.add(hdr + " " + msg); + } + + public List getRecognitionErrors() + { + return recognitionErrors; + } + + public void throwLastRecognitionError() throws InvalidRequestException + { + if (recognitionErrors.size() > 0) + throw new InvalidRequestException(recognitionErrors.get((recognitionErrors.size()-1))); + } + + // used by UPDATE of the counter columns to validate if '-' was supplied by user + public void validateMinusSupplied(Object op, final Term value, IntStream stream) throws MissingTokenException + { + if (op == null && (value.isBindMarker() || Long.parseLong(value.getText()) > 0)) + throw new MissingTokenException(102, stream, value); + } + +} + +@lexer::header { + package org.apache.cassandra.cql3; + + import org.apache.cassandra.thrift.InvalidRequestException; +} + +@lexer::members { + List tokens = new ArrayList(); + + public void emit(Token token) + { + state.token = token; + tokens.add(token); + } + + public Token nextToken() + { + super.nextToken(); + if (tokens.size() == 0) + return Token.EOF_TOKEN; + return tokens.remove(0); + } + + private List recognitionErrors = new ArrayList(); + + public void displayRecognitionError(String[] tokenNames, RecognitionException e) + { + String hdr = getErrorHeader(e); + String msg = getErrorMessage(e, tokenNames); + recognitionErrors.add(hdr + " " + msg); + } + + public List getRecognitionErrors() + { + return recognitionErrors; + } + + public void throwLastRecognitionError() throws InvalidRequestException + { + if (recognitionErrors.size() > 0) + throw new InvalidRequestException(recognitionErrors.get((recognitionErrors.size()-1))); + } +} + +/** STATEMENTS **/ + +query returns [CQLStatement stmnt] + : st=cqlStatement (';')* EOF { $stmnt = st; } + ; + +cqlStatement returns [CQLStatement stmt] + @after{ if (stmt != null) stmt.setBoundTerms(currentBindMarkerIdx + 1); } + : st1= selectStatement { $stmt = st1; } + | st2= insertStatement { $stmt = st2; } + | st3= updateStatement { $stmt = st3; } + | st4= batchStatement { $stmt = st4; } + | st5= deleteStatement { $stmt = st5; } + | st6= useStatement { $stmt = st6; } + | st7= truncateStatement { $stmt = st7; } + | st8= createKeyspaceStatement { $stmt = st8; } + | st9= createColumnFamilyStatement { $stmt = st9; } + | st10=createIndexStatement { $stmt = st10; } + | st11=dropKeyspaceStatement { $stmt = st11; } + | st12=dropColumnFamilyStatement { $stmt = st12; } + | st13=dropIndexStatement { $stmt = st13; } + | st14=alterTableStatement { $stmt = st14; } + ; + +/* + * USE ; + */ +useStatement returns [UseStatement stmt] + : K_USE ks=keyspaceName { $stmt = new UseStatement(ks); } + ; + +/** + * SELECT + * FROM + * USING CONSISTENCY + * WHERE KEY = "key1" AND COL > 1 AND COL < 100 + * LIMIT ; + */ +selectStatement returns [SelectStatement.RawStatement expr] + @init { + boolean isCount = false; + ConsistencyLevel cLevel = ConsistencyLevel.ONE; + int limit = 10000; + boolean reversed = false; + } + : K_SELECT ( sclause=selectClause | (K_COUNT '(' sclause=selectClause ')' { isCount = true; }) ) + K_FROM cf=columnFamilyName + ( K_USING K_CONSISTENCY K_LEVEL { cLevel = ConsistencyLevel.valueOf($K_LEVEL.text.toUpperCase()); } )? + ( K_WHERE wclause=whereClause )? + ( K_ORDER (K_ASC | K_DESC { reversed = true; }) )? + ( K_LIMIT rows=INTEGER { limit = Integer.parseInt($rows.text); } )? + { + SelectStatement.Parameters params = new SelectStatement.Parameters(cLevel, + limit, + reversed, + isCount); + $expr = new SelectStatement.RawStatement(cf, params, sclause, wclause); + } + ; + +selectClause returns [List expr] + : ids=cidentList { $expr = ids; } + | '\*' { $expr = Collections.emptyList();} + ; + +whereClause returns [List clause] + @init{ $clause = new ArrayList(); } + : first=relation { $clause.add(first); } (K_AND next=relation { $clause.add(next); })* + ; + +/** + * INSERT INTO (, , , ...) + * VALUES (, , , ...) + * USING CONSISTENCY AND TIMESTAMP ; + * + * Consistency level is set to ONE by default + */ +insertStatement returns [UpdateStatement expr] + @init { + Attributes attrs = new Attributes(); + List columnNames = new ArrayList(); + List columnValues = new ArrayList(); + } + : K_INSERT K_INTO cf=columnFamilyName + '(' c1=cident { columnNames.add(c1); } ( ',' cn=cident { columnNames.add(cn); } )+ ')' + K_VALUES + '(' v1=term { columnValues.add(v1); } ( ',' vn=term { columnValues.add(vn); } )+ ')' + ( usingClause[attrs] )? + { + $expr = new UpdateStatement(cf, columnNames, columnValues, attrs); + } + ; + +usingClause[Attributes attrs] + : K_USING usingClauseObjective[attrs] ( K_AND? usingClauseObjective[attrs] )* + ; + +usingClauseDelete[Attributes attrs] + : K_USING usingClauseDeleteObjective[attrs] ( K_AND? usingClauseDeleteObjective[attrs] )* + ; + +usingClauseDeleteObjective[Attributes attrs] + : K_CONSISTENCY K_LEVEL { attrs.cLevel = ConsistencyLevel.valueOf($K_LEVEL.text.toUpperCase()); } + | K_TIMESTAMP ts=INTEGER { attrs.timestamp = Long.valueOf($ts.text); } + ; + +usingClauseObjective[Attributes attrs] + : usingClauseDeleteObjective[attrs] + | K_TTL t=INTEGER { attrs.timeToLive = Integer.valueOf($t.text); } + ; + +/** + * UPDATE + * USING CONSISTENCY AND TIMESTAMP + * SET name1 = value1, name2 = value2 + * WHERE key = value; + */ +updateStatement returns [UpdateStatement expr] + @init { + Attributes attrs = new Attributes(); + Map columns = new HashMap(); + } + : K_UPDATE cf=columnFamilyName + ( usingClause[attrs] )? + K_SET termPairWithOperation[columns] (',' termPairWithOperation[columns])* + K_WHERE wclause=whereClause + { + return new UpdateStatement(cf, columns, wclause, attrs); + } + ; + +/** + * DELETE name1, name2 + * FROM + * USING CONSISTENCY AND TIMESTAMP + * WHERE KEY = keyname; + */ +deleteStatement returns [DeleteStatement expr] + @init { + Attributes attrs = new Attributes(); + List columnsList = Collections.emptyList(); + } + : K_DELETE ( ids=cidentList { columnsList = ids; } )? + K_FROM cf=columnFamilyName + ( usingClauseDelete[attrs] )? + K_WHERE wclause=whereClause + { + return new DeleteStatement(cf, columnsList, wclause, attrs); + } + ; + + +/** + * BEGIN BATCH [USING CONSISTENCY ] + * UPDATE SET name1 = value1 WHERE KEY = keyname1; + * UPDATE SET name2 = value2 WHERE KEY = keyname2; + * UPDATE SET name3 = value3 WHERE KEY = keyname3; + * ... + * APPLY BATCH + * + * OR + * + * BEGIN BATCH [USING CONSISTENCY ] + * INSERT INTO (KEY, ) VALUES ('', ''); + * INSERT INTO (KEY, ) VALUES ('', ''); + * ... + * APPLY BATCH + * + * OR + * + * BEGIN BATCH [USING CONSISTENCY ] + * DELETE name1, name2 FROM WHERE key = + * DELETE name3, name4 FROM WHERE key = + * ... + * APPLY BATCH + */ +batchStatement returns [BatchStatement expr] + @init { + Attributes attrs = new Attributes(); + List statements = new ArrayList(); + } + : K_BEGIN K_BATCH ( usingClause[attrs] )? + s1=batchStatementObjective ';'? { statements.add(s1); } ( sN=batchStatementObjective ';'? { statements.add(sN); } )* + K_APPLY K_BATCH + { + return new BatchStatement(statements, attrs); + } + ; + +batchStatementObjective returns [ModificationStatement statement] + : i=insertStatement { $statement = i; } + | u=updateStatement { $statement = u; } + | d=deleteStatement { $statement = d; } + ; + +/** + * CREATE KEYSPACE WITH attr1 = value1 AND attr2 = value2; + */ +createKeyspaceStatement returns [CreateKeyspaceStatement expr] + : K_CREATE K_KEYSPACE ks=keyspaceName + K_WITH props=properties { $expr = new CreateKeyspaceStatement(ks, props); } + ; + +/** + * CREATE COLUMNFAMILY ( + * , + * , + * + * ) WITH = AND ...; + */ +createColumnFamilyStatement returns [CreateColumnFamilyStatement.RawStatement expr] + : K_CREATE K_COLUMNFAMILY cf=columnFamilyName { $expr = new CreateColumnFamilyStatement.RawStatement(cf); } + cfamDefinition[expr] + ; + +cfamDefinition[CreateColumnFamilyStatement.RawStatement expr] + : '(' cfamColumns[expr] ( ',' cfamColumns[expr]? )* ')' + ( K_WITH cfamProperty[expr] ( K_AND cfamProperty[expr] )*)? + ; + +cfamColumns[CreateColumnFamilyStatement.RawStatement expr] + : k=cident v=comparatorType { $expr.addDefinition(k, v); } (K_PRIMARY K_KEY { $expr.setKeyAlias(k); })? + | K_PRIMARY K_KEY '(' k=cident { $expr.setKeyAlias(k); } (',' c=cident { $expr.addColumnAlias(c); } )* ')' + ; + +cfamProperty[CreateColumnFamilyStatement.RawStatement expr] + : k=property '=' v=propertyValue { $expr.addProperty(k, v); } + | K_COMPACT K_STORAGE { $expr.setCompactStorage(); } + ; + +/** + * CREATE INDEX [indexName] ON columnFamily (columnName); + */ +createIndexStatement returns [CreateIndexStatement expr] + : K_CREATE K_INDEX (idxName=IDENT)? K_ON cf=columnFamilyName '(' id=cident ')' + { $expr = new CreateIndexStatement(cf, $idxName.text, id); } + ; + +/** + * ALTER COLUMN FAMILY ALTER TYPE ; + * ALTER COLUMN FAMILY ADD ; + * ALTER COLUMN FAMILY DROP ; + * ALTER COLUMN FAMILY WITH = ; + */ +alterTableStatement returns [AlterTableStatement expr] + @init { + AlterTableStatement.Type type = null; + String validator = null; + ColumnIdentifier columnName = null; + Map propertyMap = null; + } + : K_ALTER K_COLUMNFAMILY cf=columnFamilyName + ( K_ALTER id=cident K_TYPE v=comparatorType { type = AlterTableStatement.Type.ALTER; } + | K_ADD id=cident v=comparatorType { type = AlterTableStatement.Type.ADD; } + | K_DROP id=cident { type = AlterTableStatement.Type.DROP; } + | K_WITH props=properties { type = AlterTableStatement.Type.OPTS; } + ) + { + $expr = new AlterTableStatement(cf, type, id, v, props); + } + ; + +/** + * DROP KEYSPACE ; + */ +dropKeyspaceStatement returns [DropKeyspaceStatement ksp] + : K_DROP K_KEYSPACE ks=keyspaceName { $ksp = new DropKeyspaceStatement(ks); } + ; + +/** + * DROP COLUMNFAMILY ; + */ +dropColumnFamilyStatement returns [DropColumnFamilyStatement stmt] + : K_DROP K_COLUMNFAMILY cf=columnFamilyName { $stmt = new DropColumnFamilyStatement(cf); } + ; + +/** + * DROP INDEX + */ +dropIndexStatement returns [DropIndexStatement expr] + : + K_DROP K_INDEX index=IDENT + { $expr = new DropIndexStatement($index.text); } + ; + +/** + * TRUNCATE ; + */ +truncateStatement returns [TruncateStatement stmt] + : K_TRUNCATE cf=columnFamilyName { $stmt = new TruncateStatement(cf); } + ; + + +/** DEFINITIONS **/ + +// Column Identifiers +cident returns [ColumnIdentifier id] + : t=( IDENT | UUID | INTEGER ) { $id = new ColumnIdentifier($t.text, false); } + | t=QUOTED_NAME { $id = new ColumnIdentifier($t.text, true); } + ; + +// Keyspace & Column family names +keyspaceName returns [String id] + @init { CFName name = new CFName(); } + : cfOrKsName[name, true] { $id = name.getKeyspace(); } + ; + +columnFamilyName returns [CFName name] + @init { $name = new CFName(); } + : (cfOrKsName[name, true] '.')? cfOrKsName[name, false] + ; + +cfOrKsName[CFName name, boolean isKs] + : t=IDENT { if (isKs) $name.setKeyspace($t.text, false); else $name.setColumnFamily($t.text, false); } + | t=QUOTED_NAME { if (isKs) $name.setKeyspace($t.text, true); else $name.setColumnFamily($t.text, true); } + ; + +cidentList returns [List items] + @init{ $items = new ArrayList(); } + : t1=cident { $items.add(t1); } (',' tN=cident { $items.add(tN); })* + ; + +// Values (includes prepared statement markers) +term returns [Term term] + : t=(STRING_LITERAL | UUID | IDENT | INTEGER | FLOAT ) { $term = new Term($t.text, $t.type); } + | t=QMARK { $term = new Term($t.text, $t.type, ++currentBindMarkerIdx); } + ; + +intTerm returns [Term integer] + : t=INTEGER { $integer = new Term($t.text, $t.type); } + | t=QMARK { $integer = new Term($t.text, $t.type, ++currentBindMarkerIdx); } + ; + +termPairWithOperation[Map columns] + : key=cident '=' + ( value=term { columns.put(key, new Operation(value)); } + | c=cident ( '+' v=intTerm { columns.put(key, new Operation(c, Operation.Type.PLUS, v)); } + | op='-'? v=intTerm + { + validateMinusSupplied(op, v, input); + if (op == null) + v = new Term(-(Long.valueOf(v.getText())), v.getType()); + columns.put(key, new Operation(c, Operation.Type.MINUS, v)); + } + ) + ) + ; + +property returns [String str] + : p=(COMPIDENT | IDENT) { $str = $p.text; } + ; + +propertyValue returns [String str] + : v=(STRING_LITERAL | IDENT | INTEGER | FLOAT) { $str = $v.text; } + ; + +properties returns [Map props] + @init{ $props = new HashMap(); } + : k1=property '=' v1=propertyValue { $props.put(k1, v1); } (K_AND kn=property '=' vn=propertyValue { $props.put(kn, vn); } )* + ; + +relation returns [Relation rel] + : name=cident type=('=' | '<' | '<=' | '>=' | '>') t=term { $rel = new Relation($name.id, $type.text, $t.term); } + | name=cident K_IN { $rel = Relation.createInRelation($name.id); } + '(' f1=term { $rel.addInValue(f1); } (',' fN=term { $rel.addInValue(fN); } )* ')' + ; + +comparatorType returns [String str] + : c=(IDENT | STRING_LITERAL) { $str = $c.text; } + ; + + +// Case-insensitive keywords +K_SELECT: S E L E C T; +K_FROM: F R O M; +K_WHERE: W H E R E; +K_AND: A N D; +K_KEY: K E Y; +K_INSERT: I N S E R T; +K_UPDATE: U P D A T E; +K_WITH: W I T H; +K_LIMIT: L I M I T; +K_USING: U S I N G; +K_CONSISTENCY: C O N S I S T E N C Y; +K_LEVEL: ( O N E + | Q U O R U M + | A L L + | A N Y + | L O C A L '_' Q U O R U M + | E A C H '_' Q U O R U M + ) + ; +K_USE: U S E; +K_COUNT: C O U N T; +K_SET: S E T; +K_BEGIN: B E G I N; +K_APPLY: A P P L Y; +K_BATCH: B A T C H; +K_TRUNCATE: T R U N C A T E; +K_DELETE: D E L E T E; +K_IN: I N; +K_CREATE: C R E A T E; +K_KEYSPACE: ( K E Y S P A C E + | S C H E M A ); +K_COLUMNFAMILY:( C O L U M N F A M I L Y + | T A B L E ); +K_INDEX: I N D E X; +K_ON: O N; +K_DROP: D R O P; +K_PRIMARY: P R I M A R Y; +K_INTO: I N T O; +K_VALUES: V A L U E S; +K_TIMESTAMP: T I M E S T A M P; +K_TTL: T T L; +K_ALTER: A L T E R; +K_ADD: A D D; +K_TYPE: T Y P E; +K_COMPACT: C O M P A C T; +K_STORAGE: S T O R A G E; +K_ORDER: O R D E R; +K_ASC: A S C; +K_DESC: D E S C; + +// Case-insensitive alpha characters +fragment A: ('a'|'A'); +fragment B: ('b'|'B'); +fragment C: ('c'|'C'); +fragment D: ('d'|'D'); +fragment E: ('e'|'E'); +fragment F: ('f'|'F'); +fragment G: ('g'|'G'); +fragment H: ('h'|'H'); +fragment I: ('i'|'I'); +fragment J: ('j'|'J'); +fragment K: ('k'|'K'); +fragment L: ('l'|'L'); +fragment M: ('m'|'M'); +fragment N: ('n'|'N'); +fragment O: ('o'|'O'); +fragment P: ('p'|'P'); +fragment Q: ('q'|'Q'); +fragment R: ('r'|'R'); +fragment S: ('s'|'S'); +fragment T: ('t'|'T'); +fragment U: ('u'|'U'); +fragment V: ('v'|'V'); +fragment W: ('w'|'W'); +fragment X: ('x'|'X'); +fragment Y: ('y'|'Y'); +fragment Z: ('z'|'Z'); + +STRING_LITERAL + @init{ StringBuilder b = new StringBuilder(); } + @after{ setText(b.toString()); } + : '\'' (c=~('\'') { b.appendCodePoint(c);} | '\'' '\'' { b.appendCodePoint('\''); })* '\'' + ; + +QUOTED_NAME + @init{ StringBuilder b = new StringBuilder(); } + @after{ setText(b.toString()); } + : '\"' (c=~('\"') { b.appendCodePoint(c); } | '\"' '\"' { b.appendCodePoint('\"'); })* '\"' + ; + +fragment DIGIT + : '0'..'9' + ; + +fragment LETTER + : ('A'..'Z' | 'a'..'z') + ; + +fragment HEX + : ('A'..'F' | 'a'..'f' | '0'..'9') + ; + +INTEGER + : '-'? DIGIT+ + ; + +QMARK + : '?' + ; + +/* + * Normally a lexer only emits one token at a time, but ours is tricked out + * to support multiple (see @lexer::members near the top of the grammar). + */ +FLOAT + : INTEGER '.' INTEGER + ; + +IDENT + : LETTER (LETTER | DIGIT | '_')* + ; + +COMPIDENT + : IDENT ( ':' (IDENT | INTEGER))+ + ; + +UUID + : HEX HEX HEX HEX HEX HEX HEX HEX '-' + HEX HEX HEX HEX '-' + HEX HEX HEX HEX '-' + HEX HEX HEX HEX '-' + HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX + ; + +WS + : (' ' | '\t' | '\n' | '\r')+ { $channel = HIDDEN; } + ; + +COMMENT + : ('--' | '//') .* ('\n'|'\r') { $channel = HIDDEN; } + ; + +MULTILINE_COMMENT + : '/*' .* '*/' { $channel = HIDDEN; } + ; diff --git a/src/java/org/apache/cassandra/cql3/Operation.java b/src/java/org/apache/cassandra/cql3/Operation.java new file mode 100644 index 0000000000..63097b9f35 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Operation.java @@ -0,0 +1,54 @@ +/* + * 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; + +public class Operation +{ + public static enum Type { PLUS, MINUS } + + public final Type type; + public final ColumnIdentifier ident; + public final Term value; + + // unary operation + public Operation(Term a) + { + this(null, null, a); + } + + // binary operation + public Operation(ColumnIdentifier a, Type type, Term b) + { + this.ident = a; + this.type = type; + this.value = b; + } + + public boolean isUnary() + { + return type == null && ident == null; + } + + public String toString() + { + return (isUnary()) + ? String.format("UnaryOperation(%s)", value) + : String.format("BinaryOperation(%s, %s, %s)", ident, type, value); + } +} diff --git a/src/java/org/apache/cassandra/cql3/QueryProcessor.java b/src/java/org/apache/cassandra/cql3/QueryProcessor.java new file mode 100644 index 0000000000..7c82616f19 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/QueryProcessor.java @@ -0,0 +1,205 @@ +/* + * 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.nio.ByteBuffer; +import java.util.*; + +import com.google.common.base.Predicates; +import com.google.common.collect.Maps; +import org.antlr.runtime.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.cql3.statements.*; +import org.apache.cassandra.config.*; +import org.apache.cassandra.db.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.*; +import org.apache.cassandra.thrift.Column; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.SemanticVersion; + +public class QueryProcessor +{ + public static final SemanticVersion CQL_VERSION = new SemanticVersion("3.0.0-beta1"); + + private static final Logger logger = LoggerFactory.getLogger(QueryProcessor.class); + + public static void validateKey(ByteBuffer key) throws InvalidRequestException + { + if (key == null || key.remaining() == 0) + { + throw new InvalidRequestException("Key may not be empty"); + } + + // check that key can be handled by FBUtilities.writeShortByteArray + if (key.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) + { + throw new InvalidRequestException("Key length of " + key.remaining() + + " is longer than maximum of " + FBUtilities.MAX_UNSIGNED_SHORT); + } + } + + public static void validateColumnNames(Iterable columns) + throws InvalidRequestException + { + for (ByteBuffer name : columns) + { + if (name.remaining() > IColumn.MAX_NAME_LENGTH) + throw new InvalidRequestException(String.format("column name is too long (%s > %s)", + name.remaining(), + IColumn.MAX_NAME_LENGTH)); + if (name.remaining() == 0) + throw new InvalidRequestException("zero-length column name"); + } + } + + public static void validateColumnName(ByteBuffer column) + throws InvalidRequestException + { + validateColumnNames(Collections.singletonList(column)); + } + + public static void validateSlicePredicate(CFMetaData metadata, SlicePredicate predicate) + throws InvalidRequestException + { + if (predicate.slice_range != null) + validateSliceRange(metadata, predicate.slice_range); + else + validateColumnNames(predicate.column_names); + } + + public static void validateSliceRange(CFMetaData metadata, SliceRange range) + throws InvalidRequestException + { + validateSliceRange(metadata, range.start, range.finish, range.reversed); + } + + public static void validateSliceRange(CFMetaData metadata, ByteBuffer start, ByteBuffer finish, boolean reversed) + throws InvalidRequestException + { + AbstractType comparator = metadata.getComparatorFor(null); + Comparator orderedComparator = reversed ? comparator.reverseComparator: comparator; + if (start.remaining() > 0 && finish.remaining() > 0 && orderedComparator.compare(start, finish) > 0) + throw new InvalidRequestException("Range finish must come after start in traversal order"); + } + + private static CqlResult processStatement(CQLStatement statement, ClientState clientState, List variables) + throws UnavailableException, InvalidRequestException, TimedOutException, SchemaDisagreementException + { + statement.checkAccess(clientState); + statement.validate(clientState); + CqlResult result = statement.execute(clientState, variables); + if (result == null) + { + result = new CqlResult(); + result.type = CqlResultType.VOID; + } + return result; + } + + public static CqlResult process(String queryString, ClientState clientState) + throws RecognitionException, UnavailableException, InvalidRequestException, TimedOutException, SchemaDisagreementException + { + logger.trace("CQL QUERY: {}", queryString); + return processStatement(getStatement(queryString, clientState), clientState, Collections.emptyList()); + } + + public static CqlPreparedResult prepare(String queryString, ClientState clientState) + throws RecognitionException, InvalidRequestException + { + logger.trace("CQL QUERY: {}", queryString); + + CQLStatement statement = getStatement(queryString, clientState); + int statementId = makeStatementId(queryString); + logger.trace("Discovered "+ statement.getBoundsTerms() + " bound variables."); + + clientState.getCQL3Prepared().put(statementId, statement); + logger.trace(String.format("Stored prepared statement #%d with %d bind markers", + statementId, + statement.getBoundsTerms())); + + return new CqlPreparedResult(statementId, statement.getBoundsTerms()); + } + + public static CqlResult processPrepared(CQLStatement statement, ClientState clientState, List variables) + throws UnavailableException, InvalidRequestException, TimedOutException, SchemaDisagreementException + { + // Check to see if there are any bound variables to verify + if (!(variables.isEmpty() && (statement.getBoundsTerms() == 0))) + { + if (variables.size() != statement.getBoundsTerms()) + throw new InvalidRequestException(String.format("there were %d markers(?) in CQL but %d bound variables", + statement.getBoundsTerms(), + variables.size())); + + // at this point there is a match in count between markers and variables that is non-zero + + if (logger.isTraceEnabled()) + for (int i = 0; i < variables.size(); i++) + logger.trace("[{}] '{}'", i+1, variables.get(i)); + } + + return processStatement(statement, clientState, variables); + } + + private static final int makeStatementId(String cql) + { + // use the hash of the string till something better is provided + return cql.hashCode(); + } + + private static CQLStatement getStatement(String queryStr, ClientState clientState) throws InvalidRequestException, RecognitionException + { + CQLStatement statement = parseStatement(queryStr); + + // Set keyspace for statement that require login + if (statement instanceof CFStatement) + ((CFStatement)statement).prepareKeyspace(clientState); + + if (statement instanceof Preprocessable) + statement = ((Preprocessable)statement).preprocess(); + + return statement; + } + + private static CQLStatement parseStatement(String queryStr) throws InvalidRequestException, RecognitionException + { + // Lexer and parser + CharStream stream = new ANTLRStringStream(queryStr); + CqlLexer lexer = new CqlLexer(stream); + TokenStream tokenStream = new CommonTokenStream(lexer); + CqlParser parser = new CqlParser(tokenStream); + + // Parse the query string to a statement instance + CQLStatement statement = parser.query(); + + // The lexer and parser queue up any errors they may have encountered + // along the way, if necessary, we turn them into exceptions here. + lexer.throwLastRecognitionError(); + parser.throwLastRecognitionError(); + + return statement; + } + +} diff --git a/src/java/org/apache/cassandra/cql3/Relation.java b/src/java/org/apache/cassandra/cql3/Relation.java new file mode 100644 index 0000000000..0724abdc97 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Relation.java @@ -0,0 +1,117 @@ +/* + * 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.List; + +/** + * Relations encapsulate the relationship between an entity of some kind, and + * a value (term). For example, > "start" or "colname1" = "somevalue". + * + */ +public class Relation +{ + private final ColumnIdentifier entity; + private final Type relationType; + private final Term value; + private final List inValues; + + public static enum Type + { + EQ, LT, LTE, GTE, GT, IN; + + public static Type forString(String s) + { + if (s.equals("=")) + return EQ; + else if (s.equals("<")) + return LT; + else if (s.equals("<=")) + return LTE; + else if (s.equals(">=")) + return GTE; + else if (s.equals(">")) + return GT; + + return null; + } + } + + private Relation(ColumnIdentifier entity, Type type, Term value, List inValues) + { + this.entity = entity; + this.relationType = type; + this.value = value; + this.inValues = inValues; + } + + /** + * Creates a new relation. + * + * @param entity the kind of relation this is; what the term is being compared to. + * @param type the type that describes how this entity relates to the value. + * @param value the value being compared. + */ + public Relation(ColumnIdentifier entity, String type, Term value) + { + this(entity, Type.forString(type), value, null); + } + + public static Relation createInRelation(ColumnIdentifier entity) + { + return new Relation(entity, Type.IN, null, new ArrayList()); + } + + public Type operator() + { + return relationType; + } + + public ColumnIdentifier getEntity() + { + return entity; + } + + public Term getValue() + { + assert relationType != Type.IN; + return value; + } + + public List getInValues() + { + assert relationType == Type.IN; + return inValues; + } + + public void addInValue(Term t) + { + inValues.add(t); + } + + @Override + public String toString() + { + if (relationType == Type.IN) + return String.format("%s IN %s", entity, inValues); + else + return String.format("%s %s %s", entity, relationType, value); + } +} diff --git a/src/java/org/apache/cassandra/cql3/Term.java b/src/java/org/apache/cassandra/cql3/Term.java new file mode 100644 index 0000000000..8648012f0d --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/Term.java @@ -0,0 +1,205 @@ +/* + * 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.nio.ByteBuffer; +import java.util.List; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.AsciiType; +import org.apache.cassandra.db.marshal.FloatType; +import org.apache.cassandra.db.marshal.IntegerType; +import org.apache.cassandra.db.marshal.LexicalUUIDType; +import org.apache.cassandra.db.marshal.MarshalException; +import org.apache.cassandra.thrift.InvalidRequestException; + +/** A term parsed from a CQL statement. */ +public class Term +{ + private final String text; + private final TermType type; + private int bindIndex = -1; + + public Term(String text, TermType type) + { + this.text = text == null ? "" : text; + this.type = type; + } + + /** + * Create new Term instance from a string, and an integer that corresponds + * with the token ID from CQLParser. + * + * @param text the text representation of the term. + * @param type the term's type as an integer token ID. + */ + public Term(String text, int type) + { + this(text == null ? "" : text, TermType.forInt(type)); + } + + public Term(long value, TermType type) + { + this(String.valueOf(value), type); + } + + public Term(String text, int type, int index) + { + this(text, type); + this.bindIndex = index; + } + + /** + * Returns the text parsed to create this term. + * + * @return the string term acquired from a CQL statement. + */ + public String getText() + { + return text; + } + + /** + * Returns the typed value, serialized to a ByteBuffer according to a + * comparator/validator. + * + * @return a ByteBuffer of the value. + * @throws InvalidRequestException if unable to coerce the string to its type. + */ + public ByteBuffer getByteBuffer(AbstractType validator, List variables) throws InvalidRequestException + { + try + { + if (!isBindMarker()) + return validator.fromString(text); + + // must be a marker term so check for a CqlBindValue stored in the term + if (bindIndex == -1) + throw new AssertionError("a marker Term was encountered with no index value"); + + ByteBuffer value = variables.get(bindIndex); + validator.validate(value); + return value; + } + catch (MarshalException e) + { + throw new InvalidRequestException(e.getMessage()); + } + } + + /** + * Returns the typed value, serialized to a ByteBuffer. + * + * @return a ByteBuffer of the value. + * @throws InvalidRequestException if unable to coerce the string to its type. + */ + public ByteBuffer getByteBuffer() throws InvalidRequestException + { + switch (type) + { + case STRING: + return AsciiType.instance.fromString(text); + case INTEGER: + return IntegerType.instance.fromString(text); + case UUID: + // we specifically want the Lexical class here, not "UUIDType," because we're supposed to have + // a uuid-shaped string here, and UUIDType also accepts integer or date strings (and turns them into version 1 uuids). + return LexicalUUIDType.instance.fromString(text); + case FLOAT: + return FloatType.instance.fromString(text); + } + + throw new IllegalStateException(); + } + + /** + * Obtain the term's type. + * + * @return the type + */ + public TermType getType() + { + return type; + } + + public boolean isBindMarker() + { + return type == TermType.QMARK; + } + + @Override + public String toString() + { + return String.format("Term(%s, type=%s)", getText(), type); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((text == null) ? 0 : text.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Term other = (Term) obj; + if (type==TermType.QMARK) return false; // markers are never equal + if (text == null) + { + if (other.text != null) + return false; + } else if (!text.equals(other.text)) + return false; + if (type != other.type) + return false; + return true; + } +} + +enum TermType +{ + STRING, INTEGER, UUID, FLOAT, QMARK; + + static TermType forInt(int type) + { + if ((type == CqlParser.STRING_LITERAL) || (type == CqlParser.IDENT)) + return STRING; + else if (type == CqlParser.INTEGER) + return INTEGER; + else if (type == CqlParser.UUID) + return UUID; + else if (type == CqlParser.FLOAT) + return FLOAT; + else if (type == CqlParser.QMARK) + return QMARK; + + // FIXME: handled scenario that should never occur. + return null; + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java new file mode 100644 index 0000000000..8a1ab42915 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/AlterTableStatement.java @@ -0,0 +1,180 @@ +/* + * 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.statements; + +import java.io.IOException; +import java.util.*; + +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.config.*; +import org.apache.cassandra.db.migration.Migration; +import org.apache.cassandra.db.migration.UpdateColumnFamily; +import org.apache.cassandra.thrift.CfDef; +import org.apache.cassandra.thrift.ColumnDef; +import org.apache.cassandra.thrift.InvalidRequestException; +import static org.apache.cassandra.thrift.ThriftValidation.validateColumnFamily; + +public class AlterTableStatement extends SchemaAlteringStatement +{ + public static enum Type + { + ADD, ALTER, DROP, OPTS + } + + public final Type oType; + public final String validator; + public final ColumnIdentifier columnName; + private final CFPropDefs cfProps = new CFPropDefs(); + + public AlterTableStatement(CFName name, Type type, ColumnIdentifier columnName, String validator, Map propertyMap) + { + super(name); + this.oType = type; + this.columnName = null; + this.validator = validator; // used only for ADD/ALTER commands + this.cfProps.addAll(propertyMap); + } + + public Migration getMigration() throws InvalidRequestException, IOException + { + try + { + CFMetaData meta = validateColumnFamily(keyspace(), columnFamily()); + CfDef thriftDef = meta.toThrift(); + + CFDefinition cfDef = meta.getCfDef(); + CFDefinition.Name name = this.oType == Type.OPTS ? null : cfDef.get(columnName); + switch (oType) + { + case ADD: + if (cfDef.isCompact) + throw new InvalidRequestException("Cannot add new column to a compact CF"); + if (name != null) + { + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with a PRIMARY KEY part", columnName)); + case COLUMN_METADATA: + throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with an existing column", columnName)); + } + } + thriftDef.column_metadata.add(new ColumnDefinition(columnName.key, + CFPropDefs.parseType(validator), + null, + null, + null).toThrift()); + break; + + case ALTER: + if (name == null) + throw new InvalidRequestException(String.format("Column %s was not found in CF %s", columnName, columnFamily())); + + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + throw new InvalidRequestException(String.format("Cannot alter PRIMARY KEY part %s", columnName)); + case VALUE_ALIAS: + thriftDef.default_validation_class = CFPropDefs.parseType(validator).toString(); + break; + case COLUMN_METADATA: + ColumnDefinition column = meta.getColumnDefinition(columnName.key); + column.setValidator(CFPropDefs.parseType(validator)); + thriftDef.column_metadata.add(column.toThrift()); + break; + } + break; + + case DROP: + if (cfDef.isCompact) + throw new InvalidRequestException("Cannot drop columns from a compact CF"); + if (name == null) + throw new InvalidRequestException(String.format("Column %s was not found in CF %s", columnName, columnFamily())); + + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + throw new InvalidRequestException(String.format("Cannot drop PRIMARY KEY part %s", columnName)); + case COLUMN_METADATA: + ColumnDef toDelete = null; + for (ColumnDef columnDef : thriftDef.column_metadata) + { + if (columnDef.name.equals(columnName.key)) + toDelete = columnDef; + } + assert toDelete != null; + thriftDef.column_metadata.remove(toDelete); + break; + } + break; + case OPTS: + if (cfProps == null) + throw new InvalidRequestException(String.format("ALTER COLUMNFAMILY WITH invoked, but no parameters found")); + + cfProps.validate(); + applyPropertiesToCfDef(thriftDef, cfProps); + break; + } + return new UpdateColumnFamily(thriftDef); + } + catch (ConfigurationException e) + { + InvalidRequestException ex = new InvalidRequestException(e.toString()); + ex.initCause(e); + throw ex; + } + } + + public static void applyPropertiesToCfDef(CfDef cfDef, CFPropDefs cfProps) throws InvalidRequestException + { + if (cfProps.hasProperty(CFPropDefs.KW_COMMENT)) + { + cfDef.comment = cfProps.get(CFPropDefs.KW_COMMENT); + } + + cfDef.read_repair_chance = cfProps.getDouble(CFPropDefs.KW_READREPAIRCHANCE, cfDef.read_repair_chance); + cfDef.gc_grace_seconds = cfProps.getInt(CFPropDefs.KW_GCGRACESECONDS, cfDef.gc_grace_seconds); + cfDef.replicate_on_write = cfProps.getBoolean(CFPropDefs.KW_REPLICATEONWRITE, cfDef.replicate_on_write); + cfDef.min_compaction_threshold = cfProps.getInt(CFPropDefs.KW_MINCOMPACTIONTHRESHOLD, cfDef.min_compaction_threshold); + cfDef.max_compaction_threshold = cfProps.getInt(CFPropDefs.KW_MAXCOMPACTIONTHRESHOLD, cfDef.max_compaction_threshold); + + if (!cfProps.compactionStrategyOptions.isEmpty()) + { + cfDef.compaction_strategy_options = new HashMap(cfProps.compactionStrategyOptions); + } + + if (!cfProps.compressionParameters.isEmpty()) + { + cfDef.compression_options = new HashMap(cfProps.compressionParameters); + } + } + + public String toString() + { + return String.format("AlterTableStatement(name=%s, type=%s, column=%s, validator=%s)", + cfName, + oType, + columnName, + validator); + } + +} diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java new file mode 100644 index 0000000000..077d76d3fc --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -0,0 +1,166 @@ +/* + * 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.statements; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.RowMutation; +import org.apache.cassandra.db.CounterMutation; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.RequestType; +import org.apache.cassandra.thrift.ThriftValidation; +import org.apache.cassandra.utils.Pair; + +/** + * A BATCH statement parsed from a CQL query. + * + */ +public class BatchStatement extends ModificationStatement +{ + // statements to execute + protected final List statements; + + /** + * Creates a new BatchStatement from a list of statements and a + * Thrift consistency level. + * + * @param statements a list of UpdateStatements + * @param attrs additional attributes for statement (CL, timestamp, timeToLive) + */ + public BatchStatement(List statements, Attributes attrs) + { + super(null, attrs); + this.statements = statements; + } + + @Override + public void prepareKeyspace(ClientState state) throws InvalidRequestException + { + for (ModificationStatement statement : statements) + statement.prepareKeyspace(state); + } + + @Override + public void checkAccess(ClientState state) throws InvalidRequestException + { + Set cfamsSeen = new HashSet(); + for (ModificationStatement statement : statements) + { + // Avoid unnecessary authorizations. + if (!(cfamsSeen.contains(statement.columnFamily()))) + { + state.hasColumnFamilyAccess(statement.keyspace(), statement.columnFamily(), Permission.WRITE); + cfamsSeen.add(statement.columnFamily()); + } + } + } + + @Override + public void validate(ClientState state) throws InvalidRequestException + { + if (getTimeToLive() != 0) + throw new InvalidRequestException("Global TTL on the BATCH statement is not supported."); + + for (ModificationStatement statement : statements) + { + if (statement.isSetConsistencyLevel()) + throw new InvalidRequestException("Consistency level must be set on the BATCH, not individual statements"); + + if (isSetTimestamp() && statement.isSetTimestamp()) + throw new InvalidRequestException("Timestamp must be set either on BATCH or individual statements"); + + if (statement.getTimeToLive() < 0) + throw new InvalidRequestException("A TTL must be greater or equal to 0"); + + ThriftValidation.validateConsistencyLevel(statement.keyspace(), getConsistencyLevel(), RequestType.WRITE); + } + } + + public List getMutations(ClientState clientState, List variables) + throws InvalidRequestException + { + + Map, RowAndCounterMutation> mutations = new HashMap, RowAndCounterMutation>(); + for (ModificationStatement statement : statements) + { + if (isSetTimestamp()) + statement.timestamp = timestamp; + + List lm = statement.getMutations(clientState, variables); + // Group mutation together, otherwise they won't get applied atomically + for (IMutation m : lm) + { + Pair key = Pair.create(m.getTable(), m.key()); + RowAndCounterMutation racm = mutations.get(key); + if (racm == null) + { + racm = new RowAndCounterMutation(); + mutations.put(key, racm); + } + + if (m instanceof CounterMutation) + { + if (racm.cm == null) + racm.cm = (CounterMutation)m; + else + racm.cm.addAll(m); + } + else + { + assert m instanceof RowMutation; + if (racm.rm == null) + racm.rm = (RowMutation)m; + else + racm.rm.addAll(m); + } + } + } + + List batch = new LinkedList(); + for (RowAndCounterMutation racm : mutations.values()) + { + if (racm.rm != null) + batch.add(racm.rm); + if (racm.cm != null) + batch.add(racm.cm); + } + return batch; + } + + public String toString() + { + return String.format("BatchStatement(statements=%s, consistency=%s)", statements, cLevel); + } + + private static class RowAndCounterMutation + { + public RowMutation rm; + public CounterMutation cm; + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/CFStatement.java b/src/java/org/apache/cassandra/cql3/statements/CFStatement.java new file mode 100644 index 0000000000..15e88d72f4 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/CFStatement.java @@ -0,0 +1,56 @@ +/* + * 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.statements; + +import org.apache.cassandra.cql3.CFName; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.InvalidRequestException; + +/** + * Abstract class for statements that apply on a given column family. + */ +public abstract class CFStatement extends CQLStatement +{ + protected final CFName cfName; + + protected CFStatement(CFName cfName) + { + this.cfName = cfName; + } + + public void prepareKeyspace(ClientState state) throws InvalidRequestException + { + if (!cfName.hasKeyspace()) + { + cfName.setKeyspace(state.getKeyspace(), true); + } + } + + public String keyspace() + { + assert cfName.hasKeyspace() : "The statement hasn't be prepared correctly"; + return cfName.getKeyspace(); + } + + public String columnFamily() + { + return cfName.getColumnFamily(); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java new file mode 100644 index 0000000000..f652a778b8 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/CreateColumnFamilyStatement.java @@ -0,0 +1,294 @@ +/* + * 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.statements; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang.StringUtils; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; + +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.config.ColumnDefinition; +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.db.ColumnFamilyType; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.migration.AddColumnFamily; +import org.apache.cassandra.db.migration.Migration; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.ThriftValidation; +import org.apache.cassandra.io.compress.CompressionParameters; + +/** A CREATE COLUMNFAMILY parsed from a CQL query statement. */ +public class CreateColumnFamilyStatement extends SchemaAlteringStatement +{ + private AbstractType comparator; + private AbstractType defaultValidator; + private AbstractType keyValidator; + + private ByteBuffer keyAlias; + private List columnAliases = new ArrayList(); + private ByteBuffer valueAlias; + + private final Map columns = new HashMap(); + private final CFPropDefs properties; + + public CreateColumnFamilyStatement(CFName name, CFPropDefs properties) + { + super(name); + this.properties = properties; + } + + // Column definitions + private Map getColumns() throws InvalidRequestException + { + Map columnDefs = new HashMap(); + + for (Map.Entry col : columns.entrySet()) + { + try + { + AbstractType validator = CFPropDefs.parseType(col.getValue()); + columnDefs.put(col.getKey().key, new ColumnDefinition(col.getKey().key, validator, null, null, null)); + } + catch (ConfigurationException e) + { + InvalidRequestException ex = new InvalidRequestException(e.toString()); + ex.initCause(e); + throw ex; + } + } + + return columnDefs; + } + + public Migration getMigration() throws InvalidRequestException, ConfigurationException, IOException + { + CFMetaData cfmd = getCFMetaData(); + ThriftValidation.validateCfDef(cfmd.toThrift(), null); + return new AddColumnFamily(cfmd); + } + + /** + * Returns a CFMetaData instance based on the parameters parsed from this + * CREATE statement, or defaults where applicable. + * + * @param keyspace keyspace to apply this column family to + * @param variables list of bound variables (for prepared statement) + * @return a CFMetaData instance corresponding to the values parsed from this statement + * @throws InvalidRequestException on failure to validate parsed parameters + */ + public CFMetaData getCFMetaData() throws InvalidRequestException + { + CFMetaData newCFMD; + try + { + newCFMD = new CFMetaData(keyspace(), + columnFamily(), + ColumnFamilyType.Standard, + comparator, + null); + + newCFMD.comment(properties.get(CFPropDefs.KW_COMMENT)) + .readRepairChance(properties.getDouble(CFPropDefs.KW_READREPAIRCHANCE, CFMetaData.DEFAULT_READ_REPAIR_CHANCE)) + .replicateOnWrite(properties.getBoolean(CFPropDefs.KW_REPLICATEONWRITE, CFMetaData.DEFAULT_REPLICATE_ON_WRITE)) + .gcGraceSeconds(properties.getInt(CFPropDefs.KW_GCGRACESECONDS, CFMetaData.DEFAULT_GC_GRACE_SECONDS)) + .defaultValidator(defaultValidator) + .minCompactionThreshold(properties.getInt(CFPropDefs.KW_MINCOMPACTIONTHRESHOLD, CFMetaData.DEFAULT_MIN_COMPACTION_THRESHOLD)) + .maxCompactionThreshold(properties.getInt(CFPropDefs.KW_MAXCOMPACTIONTHRESHOLD, CFMetaData.DEFAULT_MAX_COMPACTION_THRESHOLD)) + .mergeShardsChance(0.0) + .columnMetadata(getColumns()) + .keyValidator(keyValidator) + .keyAlias(keyAlias) + .columnAliases(columnAliases) + .valueAlias(valueAlias) + .compactionStrategyOptions(properties.compactionStrategyOptions) + .compressionParameters(CompressionParameters.create(properties.compressionParameters)) + .validate(); + } + catch (ConfigurationException e) + { + throw new InvalidRequestException(e.toString()); + } + return newCFMD; + } + + public static class RawStatement extends CFStatement implements Preprocessable + { + private final Map definitions = new HashMap(); + private final CFPropDefs properties = new CFPropDefs(); + + private final List keyAliases = new ArrayList(); + private List columnAliases = new ArrayList(); + + private boolean useCompactStorage; + private Multiset definedNames = HashMultiset.create(1); + + public RawStatement(CFName name) + { + super(name); + } + + /** + * Transform this raw statement into a CreateColumnFamilyStatement. + */ + public CreateColumnFamilyStatement preprocess() throws InvalidRequestException + { + try + { + // Column family name + if (!columnFamily().matches("\\w+")) + throw new InvalidRequestException(String.format("\"%s\" is not a valid column family name (must be alphanumeric character only: [0-9A-Za-z]+)", columnFamily())); + if (columnFamily().length() > 32) + throw new InvalidRequestException(String.format("Column family names shouldn't be more than 32 character long (got \"%s\")", columnFamily())); + + for (Multiset.Entry entry : definedNames.entrySet()) + if (entry.getCount() > 1) + throw new InvalidRequestException(String.format("Multiple definition of identifier %s", entry.getElement())); + + properties.validate(); + + CreateColumnFamilyStatement stmt = new CreateColumnFamilyStatement(cfName, properties); + stmt.setBoundTerms(getBoundsTerms()); + stmt.columns.putAll(definitions); // we'll remove what is not a column below + + // Ensure that exactly one key has been specified. + if (keyAliases.size() == 0) + throw new InvalidRequestException("You must specify a PRIMARY KEY"); + else if (keyAliases.size() > 1) + throw new InvalidRequestException("You may only specify one PRIMARY KEY"); + + stmt.keyAlias = keyAliases.get(0).key; + stmt.keyValidator = getTypeAndRemove(stmt.columns, keyAliases.get(0)); + + // Handle column aliases + if (columnAliases != null && !columnAliases.isEmpty()) + { + // If we use compact storage and have only one alias, it is a + // standard "dynamic" CF, otherwise it's a composite + if (useCompactStorage && columnAliases.size() == 1) + { + stmt.columnAliases.add(columnAliases.get(0).key); + stmt.comparator = getTypeAndRemove(stmt.columns, columnAliases.get(0)); + } + else + { + List> types = new ArrayList>(); + for (ColumnIdentifier t : columnAliases) + { + stmt.columnAliases.add(t.key); + types.add(getTypeAndRemove(stmt.columns, t)); + } + // For sparse, we must add the last UTF8 component + if (!useCompactStorage) + types.add(CFDefinition.definitionType); + + stmt.comparator = CompositeType.getInstance(types); + } + } + else + { + stmt.comparator = CFDefinition.definitionType; + } + + if (useCompactStorage) + { + // There should be only one column definition remaining, which gives us the default validator. + if (stmt.columns.isEmpty()) + throw new InvalidRequestException("COMPACT STORAGE requires one definition not part of the PRIMARY KEY, none found"); + if (stmt.columns.size() > 1) + throw new InvalidRequestException(String.format("COMPACT STORAGE allows only one column not part of the PRIMARY KEY (got: %s)", StringUtils.join(stmt.columns.keySet(), ", "))); + + Map.Entry lastEntry = stmt.columns.entrySet().iterator().next(); + stmt.defaultValidator = CFPropDefs.parseType(lastEntry.getValue()); + stmt.valueAlias = lastEntry.getKey().key; + stmt.columns.remove(lastEntry.getKey()); + } + else + { + if (stmt.columns.isEmpty()) + throw new InvalidRequestException("No definition found that is not part of the PRIMARY KEY"); + + // There is no way to insert/access a column that is not defined for non-compact + // storage, so the actual validator don't matter much. + stmt.defaultValidator = CFDefinition.definitionType; + } + + return stmt; + } + catch (ConfigurationException e) + { + throw new InvalidRequestException(e.getMessage()); + } + } + + private static AbstractType getTypeAndRemove(Map columns, ColumnIdentifier t) throws InvalidRequestException, ConfigurationException + { + String typeStr = columns.get(t); + if (typeStr == null) + throw new InvalidRequestException(String.format("Unkown definition %s referenced in PRIMARY KEY", t)); + columns.remove(t); + return CFPropDefs.parseType(typeStr); + } + + public void addDefinition(ColumnIdentifier def, String type) + { + definedNames.add(def); + definitions.put(def, type); + } + + public void setKeyAlias(ColumnIdentifier alias) + { + keyAliases.add(alias); + } + + public void addColumnAlias(ColumnIdentifier alias) + { + columnAliases.add(alias); + } + + public void addProperty(String name, String value) + { + properties.addProperty(name, value); + } + + public void setCompactStorage() + { + useCompactStorage = true; + } + + public void checkAccess(ClientState state) + { + throw new UnsupportedOperationException(); + } + + public CqlResult execute(ClientState state, List variables) + { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java new file mode 100644 index 0000000000..6be4e63011 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/CreateIndexStatement.java @@ -0,0 +1,108 @@ +/* + * 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.statements; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.db.migration.Migration; +import org.apache.cassandra.db.migration.UpdateColumnFamily; +import org.apache.cassandra.thrift.CfDef; +import org.apache.cassandra.thrift.ColumnDef; +import org.apache.cassandra.thrift.IndexType; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.ThriftValidation; + +/** A CREATE INDEX statement parsed from a CQL query. */ +public class CreateIndexStatement extends SchemaAlteringStatement +{ + private static final Logger logger = LoggerFactory.getLogger(CreateIndexStatement.class); + + private final String indexName; + private final ColumnIdentifier columnName; + + public CreateIndexStatement(CFName name, String indexName, ColumnIdentifier columnName) + { + super(name); + this.indexName = indexName; + this.columnName = columnName; + } + + public Migration getMigration() throws InvalidRequestException, ConfigurationException + { + try + { + CFMetaData oldCfm = ThriftValidation.validateColumnFamily(keyspace(), columnFamily()); + boolean columnExists = false; + // mutating oldCfm directly would be bad, but mutating a Thrift copy is fine. This also + // sets us up to use validateCfDef to check for index name collisions. + CfDef cf_def = oldCfm.toThrift(); + for (ColumnDef cd : cf_def.column_metadata) + { + if (cd.name.equals(columnName.key)) + { + if (cd.index_type != null) + throw new InvalidRequestException("Index already exists"); + if (logger.isDebugEnabled()) + logger.debug("Updating column {} definition for index {}", columnName, indexName); + cd.setIndex_type(IndexType.KEYS); + cd.setIndex_name(indexName); + columnExists = true; + break; + } + } + if (!columnExists) + { + CFDefinition cfDef = oldCfm.getCfDef(); + CFDefinition.Name name = cfDef.get(columnName); + if (name != null) + { + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + throw new InvalidRequestException(String.format("Cannot create index on PRIMARY KEY part %s", columnName)); + case VALUE_ALIAS: + throw new InvalidRequestException(String.format("Cannot create index on column %s of compact CF", columnName)); + } + } + throw new InvalidRequestException("No column definition found for column " + columnName); + } + + CFMetaData.addDefaultIndexNames(cf_def); + ThriftValidation.validateCfDef(cf_def, oldCfm); + return new UpdateColumnFamily(cf_def); + } + catch (InvalidRequestException e) + { + logger.error("oups", e); + throw e; + } + catch (ConfigurationException e) + { + logger.error("oups", e); + throw e; + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java new file mode 100644 index 0000000000..1e9349c52f --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/CreateKeyspaceStatement.java @@ -0,0 +1,114 @@ +/* + * 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.statements; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.KSMetaData; +import org.apache.cassandra.db.migration.AddKeyspace; +import org.apache.cassandra.db.migration.Migration; +import org.apache.cassandra.locator.AbstractReplicationStrategy; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.thrift.CfDef; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.KsDef; +import org.apache.cassandra.thrift.SchemaDisagreementException; +import org.apache.cassandra.thrift.ThriftValidation; + +/** A CREATE KEYSPACE statement parsed from a CQL query. */ +public class CreateKeyspaceStatement extends SchemaAlteringStatement +{ + private final String name; + private final Map attrs; + private String strategyClass; + private Map strategyOptions = new HashMap(); + + /** + * Creates a new CreateKeyspaceStatement instance for a given + * keyspace name and keyword arguments. + * + * @param name the name of the keyspace to create + * @param attrs map of the raw keyword arguments that followed the WITH keyword. + */ + public CreateKeyspaceStatement(String name, Map attrs) + { + super(); + this.name = name; + this.attrs = attrs; + } + + /** + * The CqlParser only goes as far as extracting the keyword arguments + * from these statements, so this method is responsible for processing and + * validating. + * + * @throws InvalidRequestException if arguments are missing or unacceptable + */ + @Override + public void validate(ClientState state) throws InvalidRequestException, SchemaDisagreementException + { + super.validate(state); + ThriftValidation.validateKeyspaceNotSystem(name); + + // keyspace name + if (!name.matches("\\w+")) + throw new InvalidRequestException(String.format("\"%s\" is not a valid keyspace name", name)); + if (name.length() > 32) + throw new InvalidRequestException(String.format("Keyspace names shouldn't be more than 32 character long (got \"%s\")", name)); + + // required + if (!attrs.containsKey("strategy_class")) + throw new InvalidRequestException("missing required argument \"strategy_class\""); + strategyClass = attrs.get("strategy_class"); + + // optional + for (String key : attrs.keySet()) + if ((key.contains(":")) && (key.startsWith("strategy_options"))) + strategyOptions.put(key.split(":")[1], attrs.get(key)); + + // trial run to let ARS validate class + per-class options + try + { + AbstractReplicationStrategy.createReplicationStrategy(name, + AbstractReplicationStrategy.getClass(strategyClass), + StorageService.instance.getTokenMetadata(), + DatabaseDescriptor.getEndpointSnitch(), + strategyOptions); + } + catch (ConfigurationException e) + { + throw new InvalidRequestException(e.getMessage()); + } + } + + public Migration getMigration() throws InvalidRequestException, ConfigurationException, IOException + { + KsDef ksd = new KsDef(name, strategyClass, Collections.emptyList()); + ksd.setStrategy_options(strategyOptions); + ThriftValidation.validateKsDef(ksd); + ThriftValidation.validateKeyspaceNotYetExisting(name); + return new AddKeyspace(KSMetaData.fromThrift(ksd)); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java new file mode 100644 index 0000000000..25ae4c5748 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -0,0 +1,164 @@ +/* + * 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.statements; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.RowMutation; +import org.apache.cassandra.db.filter.QueryPath; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.ThriftValidation; + +/** + * A DELETE parsed from a CQL query statement. + */ +public class DeleteStatement extends ModificationStatement +{ + private final List columns; + private final List whereClause; + + private final Map> processedKeys = new HashMap>(); + + public DeleteStatement(CFName name, List columns, List whereClause, Attributes attrs) + { + super(name, attrs); + + this.columns = columns; + this.whereClause = whereClause; + } + + public List getMutations(ClientState clientState, List variables) throws InvalidRequestException + { + clientState.hasColumnFamilyAccess(columnFamily(), Permission.WRITE); + CFMetaData metadata = ThriftValidation.validateColumnFamily(keyspace(), columnFamily()); + CFDefinition cfDef = metadata.getCfDef(); + + preprocess(cfDef); + + // Check key + List keys = processedKeys.get(cfDef.key.name); + if (keys == null || keys.isEmpty()) + throw new InvalidRequestException(String.format("Missing mandatory PRIMARY KEY part %s", cfDef.key.name)); + + ColumnNameBuilder builder = cfDef.getColumnNameBuilder(); + CFDefinition.Name firstEmpty = null; + for (CFDefinition.Name name : cfDef.columns.values()) + { + List values = processedKeys.get(name.name); + if (values == null || values.isEmpty()) + { + firstEmpty = name; + // For sparse, we must either have all component or none + if (cfDef.isComposite && !cfDef.isCompact && builder.componentCount() != 0) + throw new InvalidRequestException(String.format("Missing mandatory PRIMARY KEY part %s", name)); + } + else if (firstEmpty != null) + { + throw new InvalidRequestException(String.format("Missing PRIMARY KEY part %s since %s is set", firstEmpty, name)); + } + else + { + assert values.size() == 1; // We only allow IN for keys so far + builder.add(values.get(0), Relation.Type.EQ, variables); + } + } + + List rowMutations = new ArrayList(); + + for (Term key : keys) + { + ByteBuffer rawKey = key.getByteBuffer(cfDef.key.type, variables); + rowMutations.add(mutationForKey(cfDef, clientState, rawKey, builder, variables)); + } + + return rowMutations; + } + + public RowMutation mutationForKey(CFDefinition cfDef, ClientState clientState, ByteBuffer key, ColumnNameBuilder builder, List variables) + throws InvalidRequestException + { + RowMutation rm = new RowMutation(cfDef.cfm.ksName, key); + + if (columns.isEmpty() && builder.componentCount() == 0) + { + // No columns, delete the row + rm.delete(new QueryPath(columnFamily()), getTimestamp(clientState)); + } + else + { + for (ColumnIdentifier column : columns) + { + CFDefinition.Name name = cfDef.get(column); + if (name == null) + throw new InvalidRequestException(String.format("Unknown identifier %s", column)); + + // 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 (name.kind != CFDefinition.Name.Kind.COLUMN_METADATA && name.kind != CFDefinition.Name.Kind.VALUE_ALIAS) + throw new InvalidRequestException(String.format("Invalid identifier %s for deletion (should not be a PRIMARY KEY part)", column)); + } + + if (cfDef.isCompact) + { + ByteBuffer columnName = builder.build(); + QueryProcessor.validateColumnName(columnName); + rm.delete(new QueryPath(columnFamily(), null, columnName), getTimestamp(clientState)); + } + else + { + // Delete specific columns + Iterator iter = columns.iterator(); + while (iter.hasNext()) + { + ColumnIdentifier column = iter.next(); + ColumnNameBuilder b = iter.hasNext() ? builder.copy() : builder; + ByteBuffer columnName = b.add(column.key).build(); + QueryProcessor.validateColumnName(columnName); + rm.delete(new QueryPath(columnFamily(), null, columnName), getTimestamp(clientState)); + } + } + } + + return rm; + } + + private void preprocess(CFDefinition cfDef) throws InvalidRequestException + { + UpdateStatement.processKeys(cfDef, whereClause, processedKeys); + } + + public String toString() + { + return String.format("DeleteStatement(name=%s, columns=%s, consistency=%s keys=%s)", + cfName, + columns, + cLevel, + whereClause); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/DropColumnFamilyStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropColumnFamilyStatement.java new file mode 100644 index 0000000000..58e10b82cb --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/DropColumnFamilyStatement.java @@ -0,0 +1,39 @@ +/* + * 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.statements; + +import java.io.IOException; + +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.cql3.CFName; +import org.apache.cassandra.db.migration.DropColumnFamily; +import org.apache.cassandra.db.migration.Migration; + +public class DropColumnFamilyStatement extends SchemaAlteringStatement +{ + public DropColumnFamilyStatement(CFName name) + { + super(name); + } + + public Migration getMigration() throws ConfigurationException, IOException + { + return new DropColumnFamily(keyspace(), columnFamily()); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java new file mode 100644 index 0000000000..4e896f0331 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/DropIndexStatement.java @@ -0,0 +1,74 @@ +/* + * 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.statements; + +import java.io.IOException; + +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.config.*; +import org.apache.cassandra.db.migration.Migration; +import org.apache.cassandra.db.migration.UpdateColumnFamily; +import org.apache.cassandra.thrift.CfDef; +import org.apache.cassandra.thrift.ColumnDef; +import org.apache.cassandra.thrift.InvalidRequestException; + +public class DropIndexStatement extends SchemaAlteringStatement +{ + public final CharSequence index; + + public DropIndexStatement(String indexName) + { + super(new CFName()); + index = indexName; + } + + public Migration getMigration() throws InvalidRequestException, ConfigurationException, IOException + { + CfDef cfDef = null; + + KSMetaData ksm = Schema.instance.getTableDefinition(keyspace()); + + for (CFMetaData cfm : ksm.cfMetaData().values()) + { + cfDef = getUpdatedCFDef(cfm.toThrift()); + if (cfDef != null) + break; + } + + if (cfDef == null) + throw new InvalidRequestException("Index '" + index + "' could not be found in any of the column families of keyspace '" + keyspace() + "'"); + + return new UpdateColumnFamily(cfDef); + } + + private CfDef getUpdatedCFDef(CfDef cfDef) throws InvalidRequestException + { + for (ColumnDef column : cfDef.column_metadata) + { + if (column.index_type != null && column.index_name != null && column.index_name.equals(index)) + { + column.index_name = null; + column.index_type = null; + return cfDef; + } + } + + return null; + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java new file mode 100644 index 0000000000..1ed057d6ae --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/DropKeyspaceStatement.java @@ -0,0 +1,52 @@ +/* + * 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.statements; + +import java.io.IOException; + +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.db.migration.DropKeyspace; +import org.apache.cassandra.db.migration.Migration; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.SchemaDisagreementException; +import org.apache.cassandra.thrift.ThriftValidation; + +public class DropKeyspaceStatement extends SchemaAlteringStatement +{ + private final String keyspace; + + public DropKeyspaceStatement(String keyspace) + { + super(); + this.keyspace = keyspace; + } + + @Override + public void validate(ClientState state) throws InvalidRequestException, SchemaDisagreementException + { + super.validate(state); + ThriftValidation.validateKeyspaceNotSystem(keyspace); + } + + public Migration getMigration() throws ConfigurationException, IOException + { + return new DropKeyspace(keyspace); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java new file mode 100644 index 0000000000..c21f5ac9ab --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -0,0 +1,129 @@ +/* + * 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.statements; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.thrift.ConsistencyLevel; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.RequestType; +import org.apache.cassandra.thrift.ThriftValidation; +import org.apache.cassandra.thrift.TimedOutException; +import org.apache.cassandra.thrift.UnavailableException; + +/** + * Abstract class for statements that apply on a given column family. + */ +public abstract class ModificationStatement extends CFStatement +{ + public static final ConsistencyLevel defaultConsistency = ConsistencyLevel.ONE; + + protected final ConsistencyLevel cLevel; + protected Long timestamp; + protected final int timeToLive; + + public ModificationStatement(CFName name, Attributes attrs) + { + this(name, attrs.cLevel, attrs.timestamp, attrs.timeToLive); + } + + public ModificationStatement(CFName name, ConsistencyLevel cLevel, Long timestamp, int timeToLive) + { + super(name); + this.cLevel = cLevel; + this.timestamp = timestamp; + this.timeToLive = timeToLive; + } + + public void checkAccess(ClientState state) throws InvalidRequestException + { + state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.WRITE); + } + + @Override + public void validate(ClientState state) throws InvalidRequestException + { + if (timeToLive < 0) + throw new InvalidRequestException("A TTL must be greater or equal to 0"); + + ThriftValidation.validateConsistencyLevel(keyspace(), getConsistencyLevel(), RequestType.WRITE); + } + + public CqlResult execute(ClientState state, List variables) throws InvalidRequestException, UnavailableException, TimedOutException + { + try + { + StorageProxy.mutate(getMutations(state, variables), getConsistencyLevel()); + } + catch (TimeoutException e) + { + throw new TimedOutException(); + } + return null; + } + + public ConsistencyLevel getConsistencyLevel() + { + return (cLevel != null) ? cLevel : defaultConsistency; + } + + /** + * True if an explicit consistency level was parsed from the statement. + * + * @return true if a consistency was parsed, false otherwise. + */ + public boolean isSetConsistencyLevel() + { + return cLevel != null; + } + + public long getTimestamp(ClientState clientState) + { + return timestamp == null ? clientState.getTimestamp() : timestamp; + } + + public boolean isSetTimestamp() + { + return timestamp != null; + } + + public int getTimeToLive() + { + return timeToLive; + } + + /** + * Convert statement into a list of mutations to apply on the server + * + * @param clientState current client status + * @param variables value for prepared statement markers + * + * @return list of the mutations + * @throws InvalidRequestException on invalid requests + */ + public abstract List getMutations(ClientState clientState, List variables) throws InvalidRequestException; +} diff --git a/src/java/org/apache/cassandra/cql3/statements/Preprocessable.java b/src/java/org/apache/cassandra/cql3/statements/Preprocessable.java new file mode 100644 index 0000000000..5b5bb2ecee --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/Preprocessable.java @@ -0,0 +1,27 @@ +/* + * 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.statements; + +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.thrift.InvalidRequestException; + +public interface Preprocessable +{ + public CQLStatement preprocess() throws InvalidRequestException; +} diff --git a/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java b/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java new file mode 100644 index 0000000000..6142488bfa --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/SchemaAlteringStatement.java @@ -0,0 +1,184 @@ +/* + * 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.statements; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.db.migration.*; +import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.concurrent.StageManager; +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.config.Schema; +import org.apache.cassandra.cql3.CFName; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.SchemaDisagreementException; + +import com.google.common.base.Predicates; +import com.google.common.collect.Maps; + +/** + * Abstract class for statements that alter the schema. + */ +public abstract class SchemaAlteringStatement extends CFStatement +{ + private static final long timeLimitForSchemaAgreement = 10 * 1000; + + private final boolean isColumnFamilyLevel; + + protected SchemaAlteringStatement() + { + super(null); + this.isColumnFamilyLevel = false; + } + + protected SchemaAlteringStatement(CFName name) + { + super(name); + this.isColumnFamilyLevel = true; + } + + @Override + public void prepareKeyspace(ClientState state) throws InvalidRequestException + { + if (isColumnFamilyLevel) + super.prepareKeyspace(state); + } + + public abstract Migration getMigration() throws InvalidRequestException, IOException, ConfigurationException; + + public void checkAccess(ClientState state) throws InvalidRequestException + { + if (isColumnFamilyLevel) + state.hasColumnFamilySchemaAccess(Permission.WRITE); + else + state.hasKeyspaceSchemaAccess(Permission.WRITE); + } + + @Override + public void validate(ClientState state) throws InvalidRequestException, SchemaDisagreementException + { + validateSchemaAgreement(); + } + + public CqlResult execute(ClientState state, List variables) throws InvalidRequestException, SchemaDisagreementException + { + try + { + applyMigrationOnStage(getMigration()); + } + catch (ConfigurationException e) + { + InvalidRequestException ex = new InvalidRequestException(e.toString()); + ex.initCause(e); + throw ex; + } + catch (IOException e) + { + InvalidRequestException ex = new InvalidRequestException(e.toString()); + ex.initCause(e); + throw ex; + } + return null; + } + + // Copypasta from CassandraServer (where it is private). + private static void validateSchemaAgreement() throws SchemaDisagreementException + { + if (describeSchemaVersions().size() > 1) + throw new SchemaDisagreementException(); + } + + // Copypasta from o.a.c.thrift.CassandraDaemon + private static void applyMigrationOnStage(final Migration m) throws SchemaDisagreementException, InvalidRequestException + { + Future f = StageManager.getStage(Stage.MIGRATION).submit(new Callable() + { + public Object call() throws Exception + { + m.apply(); + m.announce(); + return null; + } + }); + try + { + f.get(); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + catch (ExecutionException e) + { + // this means call() threw an exception. deal with it directly. + if (e.getCause() != null) + { + InvalidRequestException ex = new InvalidRequestException(e.getCause().getMessage()); + ex.initCause(e.getCause()); + throw ex; + } + else + { + InvalidRequestException ex = new InvalidRequestException(e.getMessage()); + ex.initCause(e); + throw ex; + } + } + + validateSchemaIsSettled(); + } + + private static Map> describeSchemaVersions() + { + // unreachable hosts don't count towards disagreement + return Maps.filterKeys(StorageProxy.describeSchemaVersions(), + Predicates.not(Predicates.equalTo(StorageProxy.UNREACHABLE))); + } + + private static void validateSchemaIsSettled() throws SchemaDisagreementException + { + long limit = System.currentTimeMillis() + timeLimitForSchemaAgreement; + + outer: + while (limit - System.currentTimeMillis() >= 0) + { + String currentVersionId = Schema.instance.getVersion().toString(); + for (String version : describeSchemaVersions().keySet()) + { + if (!version.equals(currentVersionId)) + continue outer; + } + + // schemas agree + return; + } + + throw new SchemaDisagreementException(); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java new file mode 100644 index 0000000000..71b22bc150 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -0,0 +1,972 @@ +/* + * 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.statements; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.db.IColumn; +import org.apache.cassandra.db.CounterColumn; +import org.apache.cassandra.db.RangeSliceCommand; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.Row; +import org.apache.cassandra.db.RowPosition; +import org.apache.cassandra.db.SliceByNamesReadCommand; +import org.apache.cassandra.db.SliceFromReadCommand; +import org.apache.cassandra.db.Table; +import org.apache.cassandra.db.context.CounterContext; +import org.apache.cassandra.db.filter.QueryPath; +import org.apache.cassandra.db.marshal.CompositeType; +import org.apache.cassandra.db.marshal.TypeParser; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.dht.ExcludingBounds; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.IncludingExcludingBounds; +import org.apache.cassandra.dht.RandomPartitioner; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.thrift.Column; +import org.apache.cassandra.thrift.ConsistencyLevel; +import org.apache.cassandra.thrift.CqlMetadata; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.CqlResultType; +import org.apache.cassandra.thrift.CqlRow; +import org.apache.cassandra.thrift.IndexExpression; +import org.apache.cassandra.thrift.IndexOperator; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.RequestType; +import org.apache.cassandra.thrift.SlicePredicate; +import org.apache.cassandra.thrift.SliceRange; +import org.apache.cassandra.thrift.ThriftValidation; +import org.apache.cassandra.thrift.TimedOutException; +import org.apache.cassandra.thrift.UnavailableException; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.Pair; + +/** + * Encapsulates a completely parsed SELECT query, including the target + * column family, expression, result count, and ordering clause. + * + */ +public class SelectStatement extends CQLStatement +{ + private static final Logger logger = LoggerFactory.getLogger(SelectStatement.class); + private final static ByteBuffer countColumn = ByteBufferUtil.bytes("count"); + + public final CFDefinition cfDef; + public final Parameters parameters; + private final List> selectedNames = new ArrayList>(); // empty => wildcard + private final Map restrictions = new HashMap(); + private boolean hasIndexedExpression; + + public SelectStatement(CFDefinition cfDef, Parameters parameters) + { + this.cfDef = cfDef; + this.parameters = parameters; + } + + public void checkAccess(ClientState state) throws InvalidRequestException + { + state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.READ); + } + + public CqlResult execute(ClientState state, List variables) throws InvalidRequestException, UnavailableException, TimedOutException + { + List rows; + if (isKeyRange()) + { + rows = multiRangeSlice(variables); + } + else + { + rows = getSlice(variables); + } + + CqlResult result = new CqlResult(); + result.type = CqlResultType.ROWS; + + // count resultset is a single column named "count" + if (parameters.isCount) + { + result.schema = new CqlMetadata(Collections.emptyMap(), + Collections.emptyMap(), + "AsciiType", + "LongType"); + List columns = Collections.singletonList(new Column(countColumn).setValue(ByteBufferUtil.bytes((long) rows.size()))); + result.rows = Collections.singletonList(new CqlRow(countColumn, columns)); + return result; + } + else + { + // otherwise create resultset from query results + result.schema = new CqlMetadata(new HashMap(), + new HashMap(), + TypeParser.getShortName(cfDef.cfm.comparator), + TypeParser.getShortName(cfDef.cfm.getDefaultValidator())); + result.rows = process(rows, result.schema); + return result; + } + } + + public String keyspace() + { + return cfDef.cfm.ksName; + } + + public String columnFamily() + { + return cfDef.cfm.cfName; + } + + private List getSlice(List variables) throws InvalidRequestException, TimedOutException, UnavailableException + { + QueryPath queryPath = new QueryPath(columnFamily()); + List commands = new ArrayList(); + + // ...a range (slice) of column names + if (isColumnRange()) + { + ByteBuffer start =getRequestedBound(true, variables); + ByteBuffer finish = getRequestedBound(false, variables); + + // Note that we use the total limit for every key. This is + // potentially inefficient, but then again, IN + LIMIT is not a + // very sensible choice + for (ByteBuffer key : getKeys(variables)) + { + QueryProcessor.validateKey(key); + QueryProcessor.validateSliceRange(cfDef.cfm, start, finish, parameters.isColumnsReversed); + commands.add(new SliceFromReadCommand(keyspace(), + key, + queryPath, + start, + finish, + parameters.isColumnsReversed, + getLimit())); + } + } + // ...of a list of column names + else + { + Collection columnNames = getRequestedColumns(variables); + QueryProcessor.validateColumnNames(columnNames); + + for (ByteBuffer key: getKeys(variables)) + { + QueryProcessor.validateKey(key); + commands.add(new SliceByNamesReadCommand(keyspace(), key, queryPath, columnNames)); + } + } + + try + { + return StorageProxy.read(commands, parameters.consistencyLevel); + } + catch (TimeoutException e) + { + throw new TimedOutException(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + private List multiRangeSlice(List variables) throws InvalidRequestException, TimedOutException, UnavailableException + { + List rows; + IPartitioner p = StorageService.getPartitioner(); + + ByteBuffer startKeyBytes = getKeyStart(variables); + ByteBuffer finishKeyBytes = getKeyFinish(variables); + + RowPosition startKey = RowPosition.forKey(startKeyBytes, p); + RowPosition finishKey = RowPosition.forKey(finishKeyBytes, p); + if (startKey.compareTo(finishKey) > 0 && !finishKey.isMinimum(p)) + { + if (p instanceof RandomPartitioner) + throw new InvalidRequestException("Start key sorts after end key. This is not allowed; you probably should not specify end key at all, under RandomPartitioner"); + else + throw new InvalidRequestException("Start key must sort before (or equal to) finish key in your partitioner!"); + } + AbstractBounds bounds; + if (includeStartKey()) + { + bounds = includeFinishKey() + ? new Bounds(startKey, finishKey) + : new IncludingExcludingBounds(startKey, finishKey); + } + else + { + bounds = includeFinishKey() + ? new Range(startKey, finishKey) + : new ExcludingBounds(startKey, finishKey); + } + + // XXX: Our use of Thrift structs internally makes me Sad. :( + SlicePredicate thriftSlicePredicate = makeSlicePredicate(variables); + QueryProcessor.validateSlicePredicate(cfDef.cfm, thriftSlicePredicate); + + List expressions = getIndexExpressions(variables); + + try + { + rows = StorageProxy.getRangeSlice(new RangeSliceCommand(keyspace(), + columnFamily(), + null, + thriftSlicePredicate, + bounds, + expressions, + getLimit(), + true), // limit by columns, not keys + parameters.consistencyLevel); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + catch (TimeoutException e) + { + throw new TimedOutException(); + } + return rows; + } + + private SlicePredicate makeSlicePredicate(List variables) + throws InvalidRequestException + { + SlicePredicate thriftSlicePredicate = new SlicePredicate(); + + if (isColumnRange()) + { + SliceRange sliceRange = new SliceRange(); + sliceRange.start = getRequestedBound(true, variables); + sliceRange.finish = getRequestedBound(false, variables); + sliceRange.reversed = parameters.isColumnsReversed; + sliceRange.count = -1; // We use this for range slices, where the count is ignored in favor of the global column count + thriftSlicePredicate.slice_range = sliceRange; + } + else + { + thriftSlicePredicate.column_names = getRequestedColumns(variables); + } + return thriftSlicePredicate; + } + + private int getLimit() + { + // For sparse, we'll end up merging all defined colums into the same CqlRow. Thus we should query up + // to 'defined columns' * 'asked limit' to be sure to have enough columns. We'll trim after query if + // this end being too much. + return cfDef.isCompact ? parameters.limit : cfDef.metadata.size() * parameters.limit; + } + + private boolean isKeyRange() + { + if (hasIndexedExpression) + return true; + + Restriction r = restrictions.get(cfDef.key.name); + return r == null || !r.isEquality(); + } + + private Collection getKeys(final List variables) throws InvalidRequestException + { + final Restriction r = restrictions.get(cfDef.key.name); + if (r == null || !r.isEquality()) + throw new IllegalStateException(); + + List keys = new ArrayList(r.eqValues.size()); + for (Term t : r.eqValues) + keys.add(t.getByteBuffer(cfDef.key.type, variables)); + return keys; + } + + private ByteBuffer getKeyStart(List variables) throws InvalidRequestException + { + Restriction r = restrictions.get(cfDef.key.name); + if (r == null) + { + return null; + } + else if (r.isEquality()) + { + assert r.eqValues.size() == 1; + return r.eqValues.get(0).getByteBuffer(cfDef.key.type, variables); + } + else + { + return r.start == null ? ByteBufferUtil.EMPTY_BYTE_BUFFER : r.start.getByteBuffer(cfDef.key.type, variables); + } + } + + private boolean includeStartKey() + { + Restriction r = restrictions.get(cfDef.key.name); + if (r == null || r.isEquality()) + return true; + else + return r.startInclusive; + } + + private ByteBuffer getKeyFinish(List variables) throws InvalidRequestException + { + Restriction r = restrictions.get(cfDef.key.name); + if (r == null) + { + return null; + } + else if (r.isEquality()) + { + assert r.eqValues.size() == 1; + return r.eqValues.get(0).getByteBuffer(cfDef.key.type, variables); + } + else + { + return r.end == null ? ByteBufferUtil.EMPTY_BYTE_BUFFER : r.end.getByteBuffer(cfDef.key.type, variables); + } + } + + private boolean includeFinishKey() + { + Restriction r = restrictions.get(cfDef.key.name); + if (r == null || r.isEquality()) + return true; + else + return r.endInclusive; + } + + private boolean isColumnRange() + { + // Static CF never entails a column slice + if (!cfDef.isCompact && !cfDef.isComposite) + return false; + + // Otherwise, it is a range query if it has at least one the column alias + // for which no relation is defined or is not EQ. + for (CFDefinition.Name name : cfDef.columns.values()) + { + Restriction r = restrictions.get(name.name); + if (r == null || !r.isEquality()) + return true; + } + return false; + } + + private boolean isWildcard() + { + return selectedNames.isEmpty(); + } + + private List getRequestedColumns(List variables) throws InvalidRequestException + { + assert !isColumnRange(); + + ColumnNameBuilder builder = cfDef.getColumnNameBuilder(); + for (CFDefinition.Name name : cfDef.columns.values()) + { + Restriction r = restrictions.get(name.name); + assert r != null && r.isEquality() && r.eqValues.size() == 1; + builder.add(r.eqValues.get(0), Relation.Type.EQ, variables); + } + + if (cfDef.isCompact) + { + return Collections.singletonList(builder.build()); + } + else + { + List columns = new ArrayList(); + // Adds all (requested) columns + Iterator> iter = getExpandedSelection().iterator(); + while (iter.hasNext()) + { + CFDefinition.Name name = iter.next().left; + // Skip everything that is not a 'metadata' column + if (name.kind != CFDefinition.Name.Kind.COLUMN_METADATA) + continue; + ColumnNameBuilder b = iter.hasNext() ? builder.copy() : builder; + ByteBuffer cname = b.add(name.name.key).build(); + columns.add(cname); + } + return columns; + } + } + + private ByteBuffer getRequestedBound(boolean isStart, List variables) throws InvalidRequestException + { + assert isColumnRange(); + + ColumnNameBuilder builder = cfDef.getColumnNameBuilder(); + for (CFDefinition.Name name : cfDef.columns.values()) + { + Restriction r = restrictions.get(name.name); + if (r == null) + { + // There wasn't any non EQ relation on that key, we select all records having the preceding component as prefix. + // For composites, if there was preceding component and we're computing the end, we must change the last component + // End-Of-Component, otherwise we would be selecting only one record. + if (builder.componentCount() > 0 && !isStart) + return builder.buildAsEndOfRange(); + else + return builder.build(); + } + + if (r.isEquality()) + { + assert r.eqValues.size() == 1; + builder.add(r.eqValues.get(0), Relation.Type.EQ, variables); + } + else + { + Term t = isStart ? r.start : r.end; + Relation.Type op = isStart + ? (r.startInclusive ? Relation.Type.GTE : Relation.Type.GT) + : (r.endInclusive ? Relation.Type.LTE : Relation.Type.LT); + if (t == null) + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + else + return builder.add(t, op, variables).build(); + } + } + // Means no relation at all or everything was an equal + return builder.build(); + } + + private List getIndexExpressions(List variables) throws InvalidRequestException + { + if (!hasIndexedExpression) + return Collections.emptyList(); + + List expressions = new ArrayList(); + for (CFDefinition.Name name : cfDef.metadata.values()) + { + Restriction restriction = restrictions.get(name.name); + if (restriction == null) + continue; + + if (restriction.isEquality()) + { + for (Term t : restriction.eqValues) + { + ByteBuffer value = t.getByteBuffer(name.type, variables); + expressions.add(new IndexExpression(name.name.key, IndexOperator.EQ, value)); + } + } + else + { + if (restriction.start != null) + { + ByteBuffer value = restriction.start.getByteBuffer(name.type, variables); + expressions.add(new IndexExpression(name.name.key, restriction.startInclusive ? IndexOperator.GTE : IndexOperator.GT, value)); + } + if (restriction.end != null) + { + ByteBuffer value = restriction.end.getByteBuffer(name.type, variables); + expressions.add(new IndexExpression(name.name.key, restriction.endInclusive ? IndexOperator.GTE : IndexOperator.GT, value)); + } + } + } + return expressions; + } + + private List> getExpandedSelection() + { + if (selectedNames.isEmpty()) + { + List> selection = new ArrayList>(); + for (CFDefinition.Name name : cfDef) + selection.add(Pair.create(name, name.name)); + return selection; + } + else + { + return selectedNames; + } + } + + private ByteBuffer value(IColumn c) + { + return (c instanceof CounterColumn) + ? ByteBufferUtil.bytes(CounterContext.instance().total(c.value())) + : c.value(); + } + + private void addToSchema(CqlMetadata schema, Pair p) + { + ByteBuffer nameAsRequested = p.right.key; + schema.name_types.put(nameAsRequested, TypeParser.getShortName(cfDef.cfm.comparator)); + schema.value_types.put(nameAsRequested, TypeParser.getShortName(p.left.type)); + } + + private List process(List rows, CqlMetadata schema) + { + List cqlRows = new ArrayList(); + List> selection = getExpandedSelection(); + List thriftColumns = null; + + for (org.apache.cassandra.db.Row row : rows) + { + if (cfDef.isCompact) + { + // One cqlRow per column + if (row.cf == null) + continue; + + for (IColumn c : row.cf.getSortedColumns()) + { + if (c.isMarkedForDelete()) + continue; + + thriftColumns = new ArrayList(); + + ByteBuffer[] components = cfDef.isComposite + ? ((CompositeType)cfDef.cfm.comparator).split(c.name()) + : null; + + // Respect selection order + for (Pair p : selection) + { + CFDefinition.Name name = p.left; + ByteBuffer nameAsRequested = p.right.key; + + addToSchema(schema, p); + Column col = new Column(nameAsRequested); + switch (name.kind) + { + case KEY_ALIAS: + col.setValue(row.key.key).setTimestamp(-1L); + break; + case COLUMN_ALIAS: + col.setTimestamp(c.timestamp()); + if (cfDef.isComposite) + { + if (name.compositePosition < components.length) + col.setValue(components[name.compositePosition]); + else + col.setValue(ByteBufferUtil.EMPTY_BYTE_BUFFER); + } + else + { + col.setValue(c.name()); + } + break; + case VALUE_ALIAS: + col.setValue(value(c)).setTimestamp(c.timestamp()); + break; + case COLUMN_METADATA: + // This should not happen for compact CF + throw new AssertionError(); + } + thriftColumns.add(col); + } + cqlRows.add(new CqlRow(row.key.key, thriftColumns)); + } + } + else if (cfDef.isComposite) + { + // Sparse case: group column in cqlRow when composite prefix is equal + if (row.cf == null) + continue; + + CompositeType composite = (CompositeType)cfDef.cfm.comparator; + int last = composite.types.size() - 1; + + ByteBuffer[] previous = null; + Map group = new HashMap(); + for (IColumn c : row.cf) + { + if (c.isMarkedForDelete()) + continue; + + ByteBuffer[] current = composite.split(c.name()); + // If current differs from previous, we've just finished a group + if (previous != null && !isSameRow(previous, current)) + { + cqlRows.add(handleGroup(selection, row.key.key, previous, group, schema)); + group = new HashMap(); + } + + // Accumulate the current column + group.put(current[last], c); + previous = current; + } + // Handle the last group + if (previous != null) + cqlRows.add(handleGroup(selection, row.key.key, previous, group, schema)); + } + else + { + // Static case: One cqlRow for all columns + thriftColumns = new ArrayList(); + // Respect selection order + for (Pair p : selection) + { + CFDefinition.Name name = p.left; + ByteBuffer nameAsRequested = p.right.key; + + if (name.kind == CFDefinition.Name.Kind.KEY_ALIAS) + { + addToSchema(schema, p); + thriftColumns.add(new Column(nameAsRequested).setValue(row.key.key).setTimestamp(-1L)); + continue; + } + + if (row.cf == null) + continue; + + addToSchema(schema, p); + IColumn c = row.cf.getColumn(name.name.key); + Column col = new Column(name.name.key); + if (c != null && !c.isMarkedForDelete()) + col.setValue(value(c)).setTimestamp(c.timestamp()); + thriftColumns.add(col); + } + cqlRows.add(new CqlRow(row.key.key, thriftColumns)); + } + } + // We don't allow reversed on range scan, but we do on multiget (IN (...)), so let's reverse the rows there too. + if (parameters.isColumnsReversed) + Collections.reverse(cqlRows); + + cqlRows = cqlRows.size() > parameters.limit ? cqlRows.subList(0, parameters.limit) : cqlRows; + + // Trim result if needed to respect the limit + return cqlRows; + } + + /** + * For sparse composite, returns wheter two columns belong to the same + * cqlRow base on the full list of component in the name. + * Two columns do belong together if they differ only by the last + * component. + */ + private static boolean isSameRow(ByteBuffer[] c1, ByteBuffer[] c2) + { + // Cql don't allow to insert columns who doesn't have all component of + // the composite set for sparse composite. Someone coming from thrift + // could hit that though. But since we have no way to handle this + // correctly, better fail here and tell whomever may hit that (if + // someone ever do) to change the definition to a dense composite + assert c1.length == c2.length : "Sparse composite should not have partial column names"; + for (int i = 0; i < c1.length - 1; i++) + { + if (!c1[i].equals(c2[i])) + return false; + } + return true; + } + + private CqlRow handleGroup(List> selection, ByteBuffer key, ByteBuffer[] components, Map columns, CqlMetadata schema) + { + List thriftColumns = new ArrayList(); + + // Respect requested order + for (Pair p : selection) + { + CFDefinition.Name name = p.left; + ByteBuffer nameAsRequested = p.right.key; + + addToSchema(schema, p); + Column col = new Column(nameAsRequested); + switch (name.kind) + { + case KEY_ALIAS: + col.setValue(key).setTimestamp(-1L); + break; + case COLUMN_ALIAS: + col.setValue(components[name.compositePosition]); + col.setTimestamp(-1L); + break; + case VALUE_ALIAS: + // This should not happen for SPARSE + throw new AssertionError(); + case COLUMN_METADATA: + IColumn c = columns.get(name.name.key); + if (c != null && !c.isMarkedForDelete()) + col.setValue(value(c)).setTimestamp(c.timestamp()); + break; + } + thriftColumns.add(col); + } + return new CqlRow(key, thriftColumns); + } + + public static class RawStatement extends CFStatement implements Preprocessable + { + private final Parameters parameters; + private final List selectClause; + private final List whereClause; + + public RawStatement(CFName cfName, Parameters parameters, List selectClause, List whereClause) + { + super(cfName); + this.parameters = parameters; + this.selectClause = selectClause; + this.whereClause = whereClause == null ? Collections.emptyList() : whereClause; + } + + public SelectStatement preprocess() throws InvalidRequestException + { + CFMetaData cfm = ThriftValidation.validateColumnFamily(keyspace(), columnFamily()); + ThriftValidation.validateConsistencyLevel(keyspace(), parameters.consistencyLevel, RequestType.READ); + + if (parameters.limit <= 0) + throw new InvalidRequestException("LIMIT must be strictly positive"); + + CFDefinition cfDef = cfm.getCfDef(); + SelectStatement stmt = new SelectStatement(cfDef, parameters); + stmt.setBoundTerms(getBoundsTerms()); + + // Select clause + if (parameters.isCount) + { + if (selectClause.size() != 1 || (!selectClause.get(0).equals("*") && !selectClause.get(0).equals("1"))) + throw new InvalidRequestException("Only COUNT(*) and COUNT(1) operations are currently supported."); + } + else + { + for (ColumnIdentifier t : selectClause) + { + CFDefinition.Name name = cfDef.get(t); + if (name == null) + throw new InvalidRequestException(String.format("Undefined name %s in selection clause", t)); + // Keeping the case (as in 'case sensitive') of the input name for the resultSet + stmt.selectedNames.add(Pair.create(name, t)); + } + } + + /* + * 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 conflics 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 rel : whereClause) + { + CFDefinition.Name name = cfDef.get(rel.getEntity()); + if (name == null) + throw new InvalidRequestException(String.format("Undefined name %s in where clause ('%s')", rel.getEntity(), rel)); + + if (name.kind == CFDefinition.Name.Kind.VALUE_ALIAS) + throw new InvalidRequestException(String.format("Restricting the value of a compact CF (%s) is not supported", name.name)); + + Restriction restriction = stmt.restrictions.get(name.name); + switch (rel.operator()) + { + case EQ: + if (restriction != null) + throw new InvalidRequestException(String.format("%s cannot be restricted by more than one relation if it includes an Equal", name)); + stmt.restrictions.put(name.name, new Restriction(Collections.singletonList(rel.getValue()))); + break; + case GT: + case GTE: + if (name.kind == CFDefinition.Name.Kind.KEY_ALIAS && !StorageService.getPartitioner().preservesOrder()) + throw new InvalidRequestException("Only EQ and IN relation are supported on first component of the PRIMARY KEY for RandomPartitioner"); + if (restriction == null) + { + restriction = new Restriction(); + stmt.restrictions.put(name.name, restriction); + } + if (restriction.start != null) + throw new InvalidRequestException(String.format("%s cannot be restricted by more than one Greater-Than relation", name)); + restriction.start = rel.getValue(); + if (rel.operator() == Relation.Type.GTE) + restriction.startInclusive = true; + break; + case LT: + case LTE: + if (name.kind == CFDefinition.Name.Kind.KEY_ALIAS && !StorageService.getPartitioner().preservesOrder()) + throw new InvalidRequestException("Only EQ and IN relation are supported on first component of the PRIMARY KEY for RandomPartitioner"); + if (restriction == null) + { + restriction = new Restriction(); + stmt.restrictions.put(name.name, restriction); + } + if (restriction.end != null) + throw new InvalidRequestException(String.format("%s cannot be restricted by more than one Lesser-Than relation", name)); + restriction.end = rel.getValue(); + if (rel.operator() == Relation.Type.LTE) + restriction.endInclusive = true; + break; + case IN: + if (restriction != null) + throw new InvalidRequestException(String.format("%s cannot be restricted by more than one reation if it includes a IN", name)); + if (name.kind != CFDefinition.Name.Kind.KEY_ALIAS) + throw new InvalidRequestException("IN relation can only be applied to the first component of the PRIMARY KEY"); + stmt.restrictions.put(name.name, new Restriction(rel.getInValues())); + break; + } + } + + /* + * At this point, the select statement if fully constructed, but we still have a few things to validate + */ + + // If a component of the PRIMARY KEY is restricted by a non-EQ relation, all preceding + // components must have a EQ, and all following must have no restriction + boolean shouldBeDone = false; + CFDefinition.Name previous = null; + for (CFDefinition.Name cname : cfDef.columns.values()) + { + Restriction restriction = stmt.restrictions.get(cname.name); + if (restriction == null) + shouldBeDone = true; + else if (shouldBeDone) + throw new InvalidRequestException(String.format("PRIMARY KEY part %s cannot be restricted (preceding part %s is either not restricted or by a non-EQ relation)", cname, previous)); + else if (!restriction.isEquality()) + shouldBeDone = true; + // We could support IN for the last name, we don't yet + else if (restriction.eqValues.size() > 1) + throw new InvalidRequestException(String.format("PRIMARY KEY part %s cannot be restricted by IN relation", cname)); + + previous = cname; + } + + // Deal with indexed columns + if (!cfDef.metadata.values().isEmpty()) + { + boolean hasEq = false; + Set indexed = Table.open(keyspace()).getColumnFamilyStore(columnFamily()).indexManager.getIndexedColumns(); + + for (CFDefinition.Name name : cfDef.metadata.values()) + { + Restriction restriction = stmt.restrictions.get(name.name); + if (restriction == null) + continue; + + stmt.hasIndexedExpression = true; + if (restriction.isEquality() && indexed.contains(name.name.key)) + { + hasEq = true; + break; + } + } + + if (stmt.hasIndexedExpression && !hasEq) + throw new InvalidRequestException("No indexed columns present in by-columns clause with Equal operator"); + + // If we have indexed columns and the key = X clause, we transform it into a key >= X AND key <= X clause. + // If it's a IN relation however, we reject it. + Restriction r = stmt.restrictions.get(cfDef.key.name); + if (r != null && r.isEquality()) + { + if (r.eqValues.size() > 1) + throw new InvalidRequestException("Select on indexed columns and with IN clause for the PRIMARY KEY are not supported"); + + r.start = r.eqValues.get(0); + r.startInclusive = true; + r.end = r.eqValues.get(0); + r.endInclusive = true; + r.eqValues = null; + } + } + + // Only allow reversed if the row key restriction is an equality, + // since we don't know how to reverse otherwise + if (stmt.parameters.isColumnsReversed) + { + Restriction r = stmt.restrictions.get(cfDef.key.name); + if (r == null || !r.isEquality()) + throw new InvalidRequestException("Descending order is only supported is the first part of the PRIMARY KEY is restricted by an Equal or a IN"); + } + return stmt; + } + + @Override + public String toString() + { + return String.format("SelectRawStatement[name=%s, selectClause=%s, whereClause=%s, isCount=%s, cLevel=%s, limit=%s]", + cfName, + selectClause, + whereClause, + parameters.isCount, + parameters.consistencyLevel, + parameters.limit); + } + + public void checkAccess(ClientState state) + { + throw new UnsupportedOperationException(); + } + + public CqlResult execute(ClientState state, List variables) + { + throw new UnsupportedOperationException(); + } + } + + // A rather raw class that simplify validation and query for select + // Don't made public as this can be easily badly used + private static class Restriction + { + // for equality + List eqValues; // if null, it's a restriction by bounds + + Restriction(List values) + { + this.eqValues = values; + } + + // for bounds + Term start; + boolean startInclusive; + Term end; + boolean endInclusive; + + Restriction() + { + this(null); + } + + boolean isEquality() + { + return eqValues != null; + } + } + + public static class Parameters + { + private final int limit; + private final ConsistencyLevel consistencyLevel; + private final boolean isColumnsReversed; + private final boolean isCount; + + public Parameters(ConsistencyLevel consistency, int limit, boolean reversed, boolean isCount) + { + this.consistencyLevel = consistency; + this.limit = limit; + this.isColumnsReversed = reversed; + this.isCount = isCount; + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java new file mode 100644 index 0000000000..d4c19c702b --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/TruncateStatement.java @@ -0,0 +1,68 @@ +/* + * 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.statements; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import org.apache.cassandra.auth.Permission; +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.service.StorageProxy; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.thrift.ThriftValidation; +import org.apache.cassandra.thrift.UnavailableException; + +public class TruncateStatement extends CFStatement +{ + public TruncateStatement(CFName name) + { + super(name); + } + + public void checkAccess(ClientState state) throws InvalidRequestException + { + state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.WRITE); + } + + public void validate(ClientState state) throws InvalidRequestException + { + ThriftValidation.validateColumnFamily(keyspace(), columnFamily()); + } + + public CqlResult execute(ClientState state, List variables) throws InvalidRequestException, UnavailableException + { + try + { + StorageProxy.truncateBlocking(keyspace(), columnFamily()); + } + catch (TimeoutException e) + { + throw (UnavailableException) new UnavailableException().initCause(e); + } + catch (IOException e) + { + throw (UnavailableException) new UnavailableException().initCause(e); + } + return null; + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java new file mode 100644 index 0000000000..14fed1be40 --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -0,0 +1,359 @@ +/* + * 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.statements; + +import java.nio.ByteBuffer; +import java.util.*; + +import org.apache.cassandra.cql3.*; +import org.apache.cassandra.config.CFMetaData; +import org.apache.cassandra.db.CounterMutation; +import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.RowMutation; +import org.apache.cassandra.db.filter.QueryPath; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.utils.ByteBufferUtil; + +import static org.apache.cassandra.cql.QueryProcessor.validateColumnName; + +import static org.apache.cassandra.thrift.ThriftValidation.validateColumnFamily; +import static org.apache.cassandra.thrift.ThriftValidation.validateCommutativeForWrite; + +/** + * An UPDATE statement parsed from a CQL query statement. + * + */ +public class UpdateStatement extends ModificationStatement +{ + private final Map columns; + private final List columnNames; + private final List columnValues; + private final List whereClause; + + private final Map processedColumns = new HashMap(); + private final Map> processedKeys = new HashMap>(); + + /** + * Creates a new UpdateStatement from a column family name, columns map, consistency + * level, and key term. + * + * @param keyspace Keyspace (optional) + * @param columnFamily column family name + * @param columns a map of column name/values pairs + * @param whereClause the where clause + * @param attrs additional attributes for statement (CL, timestamp, timeToLive) + */ + public UpdateStatement(CFName name, + Map columns, + List whereClause, + Attributes attrs) + { + super(name, attrs); + + this.columns = columns; + this.whereClause = whereClause; + this.columnNames = null; + this.columnValues = null; + } + + /** + * Creates a new UpdateStatement from a column family name, a consistency level, + * key, and lists of column names and values. It is intended for use with the + * alternate update format, INSERT. + * + * @param keyspace Keyspace (optional) + * @param columnFamily column family name + * @param columnNames list of column names + * @param columnValues list of column values (corresponds to names) + * @param attrs additional attributes for statement (CL, timestamp, timeToLive) + */ + public UpdateStatement(CFName name, + List columnNames, + List columnValues, + Attributes attrs) + { + super(name, attrs); + + this.columnNames = columnNames; + this.columnValues = columnValues; + this.whereClause = null; + this.columns = null; + } + + /** {@inheritDoc} */ + public List getMutations(ClientState clientState, List variables) throws InvalidRequestException + { + boolean hasCommutativeOperation = false; + + if (columns != null) + { + for (Map.Entry column : columns.entrySet()) + { + if (!column.getValue().isUnary()) + hasCommutativeOperation = true; + + if (hasCommutativeOperation && column.getValue().isUnary()) + throw new InvalidRequestException("Mix of commutative and non-commutative operations is not allowed."); + } + + } + + // Deal here with the keyspace overwrite thingy to avoid mistake + CFMetaData metadata = validateColumnFamily(keyspace(), columnFamily(), hasCommutativeOperation); + if (hasCommutativeOperation) + validateCommutativeForWrite(metadata, cLevel); + + CFDefinition cfDef = metadata.getCfDef(); + preprocess(cfDef); + + // Check key + List keys = processedKeys.get(cfDef.key.name); + if (keys == null || keys.isEmpty()) + throw new InvalidRequestException(String.format("Missing mandatory PRIMARY KEY part %s", cfDef.key)); + + ColumnNameBuilder builder = cfDef.getColumnNameBuilder(); + CFDefinition.Name firstEmpty = null; + for (CFDefinition.Name name : cfDef.columns.values()) + { + List values = processedKeys.get(name.name); + if (values == null || values.isEmpty()) + { + firstEmpty = name; + // For sparse, we must have all components + if (cfDef.isComposite && !cfDef.isCompact) + throw new InvalidRequestException(String.format("Missing mandatory PRIMARY KEY part %s", name)); + } + else if (firstEmpty != null) + { + throw new InvalidRequestException(String.format("Missing PRIMARY KEY part %s since %s is set", firstEmpty.name, name.name)); + } + else + { + assert values.size() == 1; // We only allow IN for row keys so far + builder.add(values.get(0), Relation.Type.EQ, variables); + } + } + + List rowMutations = new LinkedList(); + + for (Term key: keys) + { + ByteBuffer rawKey = key.getByteBuffer(cfDef.key.type, variables); + rowMutations.add(mutationForKey(cfDef, clientState, rawKey, builder, variables)); + } + + return rowMutations; + } + + /** + * Compute a row mutation for a single key + * + * + * @param keyspace working keyspace + * @param key key to change + * @param metadata information about CF + * + * @param clientState + * @return row mutation + * + * @throws InvalidRequestException on the wrong request + */ + private IMutation mutationForKey(CFDefinition cfDef, ClientState clientState, ByteBuffer key, ColumnNameBuilder builder, List variables) + throws InvalidRequestException + { + // if true we need to wrap RowMutation into CounterMutation + boolean hasCounterColumn = false; + RowMutation rm = new RowMutation(cfDef.cfm.ksName, key); + + if (cfDef.isCompact) + { + if (builder.componentCount() == 0) + throw new InvalidRequestException(String.format("Missing PRIMARY KEY part %s", cfDef.columns.values().iterator().next())); + + Operation value = processedColumns.get(cfDef.value.name); + if (value == null) + throw new InvalidRequestException(String.format("Missing mandatory column %s", cfDef.value)); + hasCounterColumn = addToMutation(clientState, rm, builder.build(), cfDef.value, value, variables); + } + else + { + for (CFDefinition.Name name : cfDef.metadata.values()) + { + Operation value = processedColumns.get(name.name); + if (value == null) + continue; + + ByteBuffer colName = builder.copy().add(name.name.key).build(); + hasCounterColumn |= addToMutation(clientState, rm, colName, name, value, variables); + } + } + + return (hasCounterColumn) ? new CounterMutation(rm, getConsistencyLevel()) : rm; + } + + private boolean addToMutation(ClientState clientState, + RowMutation rm, + ByteBuffer colName, + CFDefinition.Name valueDef, + Operation value, + List variables) throws InvalidRequestException + { + if (value.isUnary()) + { + ByteBuffer valueBytes = value.value.getByteBuffer(valueDef.type, variables); + validateColumnName(colName); + rm.add(new QueryPath(columnFamily(), null, colName), + valueBytes, + getTimestamp(clientState), + getTimeToLive()); + return false; + } + else + { + if (!valueDef.name.equals(value.ident)) + throw new InvalidRequestException("Only expressions like X = X + are supported."); + + long val; + try + { + val = ByteBufferUtil.toLong(value.value.getByteBuffer(LongType.instance, variables)); + } + catch (NumberFormatException e) + { + throw new InvalidRequestException(String.format("'%s' is an invalid value, should be a long.", + value.value.getText())); + } + + if (value.type == Operation.Type.MINUS) + { + if (val == Long.MIN_VALUE) + throw new InvalidRequestException("The negation of " + val + " overflows supported integer precision (signed 8 bytes integer)"); + else + val = -val; + } + rm.addCounter(new QueryPath(columnFamily(), null, colName), val); + return true; + } + } + + private void preprocess(CFDefinition cfDef) throws InvalidRequestException + { + if (columns == null) + { + // Created from an INSERT + // Don't hate, validate. + if (columnNames.size() != columnValues.size()) + throw new InvalidRequestException("unmatched column names/values"); + if (columnNames.size() < 1) + throw new InvalidRequestException("no columns specified for INSERT"); + + for (int i = 0; i < columnNames.size(); i++) + { + CFDefinition.Name name = cfDef.get(columnNames.get(i)); + if (name == null) + throw new InvalidRequestException(String.format("Unknown identifier %s", columnNames.get(i))); + + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + if (processedKeys.containsKey(name.name)) + throw new InvalidRequestException(String.format("Multiple definition found for PRIMARY KEY part %s", name)); + processedKeys.put(name.name, Collections.singletonList(columnValues.get(i))); + break; + case VALUE_ALIAS: + case COLUMN_METADATA: + if (processedColumns.containsKey(name.name)) + throw new InvalidRequestException(String.format("Multiple definition found for column %s", name)); + processedColumns.put(name.name, new Operation(columnValues.get(i))); + break; + } + } + } + else + { + // Created from an UPDATE + for (Map.Entry entry : columns.entrySet()) + { + CFDefinition.Name name = cfDef.get(entry.getKey()); + if (name == null) + throw new InvalidRequestException(String.format("Unknown identifier %s", entry.getKey())); + + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + throw new InvalidRequestException(String.format("PRIMARY KEY part %s found in SET part", entry.getKey())); + case VALUE_ALIAS: + case COLUMN_METADATA: + if (processedColumns.containsKey(name.name)) + throw new InvalidRequestException(String.format("Multiple definition found for column %s", name)); + processedColumns.put(name.name, entry.getValue()); + break; + } + } + processKeys(cfDef, whereClause, processedKeys); + } + } + + // Reused by DeleteStatement + static void processKeys(CFDefinition cfDef, List keys, Map> processed) throws InvalidRequestException + { + for (Relation rel : keys) + { + CFDefinition.Name name = cfDef.get(rel.getEntity()); + if (name == null) + throw new InvalidRequestException(String.format("Unknown key identifier %s", rel.getEntity())); + + switch (name.kind) + { + case KEY_ALIAS: + case COLUMN_ALIAS: + List values; + if (rel.operator() == Relation.Type.EQ) + values = Collections.singletonList(rel.getValue()); + else if (name.kind == CFDefinition.Name.Kind.KEY_ALIAS && rel.operator() == Relation.Type.IN) + values = rel.getInValues(); + else + throw new InvalidRequestException(String.format("Invalid operator %s for key %s", rel.operator(), rel.getEntity())); + + if (processed.containsKey(name.name)) + throw new InvalidRequestException(String.format("Multiple definition found for PRIMARY KEY part %s", name)); + processed.put(name.name, values); + break; + case VALUE_ALIAS: + case COLUMN_METADATA: + throw new InvalidRequestException(String.format("PRIMARY KEY part %s found in SET part", rel.getEntity())); + } + } + } + + public String toString() + { + return String.format("UpdateStatement(name=%s, keys=%s, columns=%s, consistency=%s, timestamp=%s, timeToLive=%s)", + cfName, + whereClause, + columns, + getConsistencyLevel(), + timestamp, + timeToLive); + } +} diff --git a/src/java/org/apache/cassandra/cql3/statements/UseStatement.java b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java new file mode 100644 index 0000000000..6a63c2a53b --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/statements/UseStatement.java @@ -0,0 +1,48 @@ +/* + * 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.statements; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.thrift.CqlResult; +import org.apache.cassandra.thrift.InvalidRequestException; + +public class UseStatement extends CQLStatement +{ + private final String keyspace; + + public UseStatement(String keyspace) + { + this.keyspace = keyspace; + } + + public void checkAccess(ClientState state) + { + // No specific access + } + + public CqlResult execute(ClientState state, List variables) throws InvalidRequestException + { + state.setKeyspace(keyspace); + return null; + } +} diff --git a/src/java/org/apache/cassandra/db/CounterMutation.java b/src/java/org/apache/cassandra/db/CounterMutation.java index 324c0b6a9d..541dfc02a7 100644 --- a/src/java/org/apache/cassandra/db/CounterMutation.java +++ b/src/java/org/apache/cassandra/db/CounterMutation.java @@ -147,6 +147,15 @@ public class CounterMutation implements IMutation rm.apply(); } + public void addAll(IMutation m) + { + if (!(m instanceof CounterMutation)) + throw new IllegalArgumentException(); + + CounterMutation cm = (CounterMutation)m; + rowMutation.addAll(cm.rowMutation); + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/db/IMutation.java b/src/java/org/apache/cassandra/db/IMutation.java index 3650f51efa..eeb92ea5d5 100644 --- a/src/java/org/apache/cassandra/db/IMutation.java +++ b/src/java/org/apache/cassandra/db/IMutation.java @@ -29,4 +29,5 @@ public interface IMutation public ByteBuffer key(); public void apply() throws IOException; public String toString(boolean shallow); + public void addAll(IMutation m); } diff --git a/src/java/org/apache/cassandra/db/RowMutation.java b/src/java/org/apache/cassandra/db/RowMutation.java index 4a58ad7b04..773877bd5e 100644 --- a/src/java/org/apache/cassandra/db/RowMutation.java +++ b/src/java/org/apache/cassandra/db/RowMutation.java @@ -245,6 +245,25 @@ public class RowMutation implements IMutation, MessageProducer } } + public void addAll(IMutation m) + { + if (!(m instanceof RowMutation)) + throw new IllegalArgumentException(); + + RowMutation rm = (RowMutation)m; + if (!table_.equals(rm.table_) || !key_.equals(rm.key_)) + throw new IllegalArgumentException(); + + for (Map.Entry entry : rm.modifications_.entrySet()) + { + // It's slighty faster to assume the key wasn't present and fix if + // not in the case where it wasn't there indeed. + ColumnFamily cf = modifications_.put(entry.getKey(), entry.getValue()); + if (cf != null) + entry.getValue().resolve(cf); + } + } + /* * This is equivalent to calling commit. Applies the changes to * to the table that is obtained by calling Table.open(). diff --git a/src/java/org/apache/cassandra/db/SystemTable.java b/src/java/org/apache/cassandra/db/SystemTable.java index ef16eb0e99..99c343b50d 100644 --- a/src/java/org/apache/cassandra/db/SystemTable.java +++ b/src/java/org/apache/cassandra/db/SystemTable.java @@ -94,7 +94,7 @@ public class SystemTable rm = new RowMutation(Table.SYSTEM_TABLE, ByteBufferUtil.bytes("cql")); cf = ColumnFamily.create(Table.SYSTEM_TABLE, VERSION_CF); - cf.addColumn(new Column(ByteBufferUtil.bytes("version"), ByteBufferUtil.bytes(QueryProcessor.CQL_VERSION))); + cf.addColumn(new Column(ByteBufferUtil.bytes("version"), ByteBufferUtil.bytes(QueryProcessor.CQL_VERSION.toString()))); rm.add(cf); rm.apply(); diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java index d3f9c1a281..cfc3b2e9d8 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java @@ -108,6 +108,23 @@ public abstract class AbstractCompositeType extends AbstractType return 1; } + /** + * Split a composite column names into it's components. + */ + public ByteBuffer[] split(ByteBuffer name) + { + List l = new ArrayList(); + ByteBuffer bb = name.duplicate(); + int i = 0; + while (bb.remaining() > 0) + { + getNextComparator(i++, bb); + l.add(getWithShortLength(bb)); + bb.get(); // skip end-of-component + } + return l.toArray(new ByteBuffer[l.size()]); + } + public String getString(ByteBuffer bytes) { StringBuilder sb = new StringBuilder(); diff --git a/src/java/org/apache/cassandra/db/marshal/CompositeType.java b/src/java/org/apache/cassandra/db/marshal/CompositeType.java index 8343f2728b..3b71000cea 100644 --- a/src/java/org/apache/cassandra/db/marshal/CompositeType.java +++ b/src/java/org/apache/cassandra/db/marshal/CompositeType.java @@ -18,12 +18,17 @@ */ package org.apache.cassandra.db.marshal; +import java.io.*; import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import org.apache.cassandra.cql3.ColumnNameBuilder; +import org.apache.cassandra.cql3.Relation; +import org.apache.cassandra.cql3.Term; import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.io.util.FastByteArrayOutputStream; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.thrift.InvalidRequestException; /* * The encoding of a CompositeType column name should be: @@ -47,8 +52,7 @@ import org.apache.cassandra.config.ConfigurationException; */ public class CompositeType extends AbstractCompositeType { - // package protected for unit tests sake - final List> types; + public final List> types; // interning instances private static final Map>, CompositeType> instances = new HashMap>, CompositeType>(); @@ -138,4 +142,118 @@ public class CompositeType extends AbstractCompositeType { return getClass().getName() + TypeParser.stringifyTypeParameters(types); } + + public static class Builder implements ColumnNameBuilder + { + private final CompositeType composite; + private int current; + + private final FastByteArrayOutputStream baos = new FastByteArrayOutputStream(); + private final DataOutput out = new DataOutputStream(baos); + + public Builder(CompositeType composite) + { + this.composite = composite; + } + + private Builder(Builder b) + { + this(b.composite); + this.current = b.current; + try + { + out.write(b.baos.toByteArray()); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public Builder add(Term t, Relation.Type op, List variables) throws InvalidRequestException + { + if (current >= composite.types.size()) + throw new IllegalStateException("Composite column is already fully constructed"); + + AbstractType currentType = composite.types.get(current++); + ByteBuffer buffer = t.getByteBuffer(currentType, variables); + try + { + ByteBufferUtil.writeWithShortLength(buffer, out); + + /* + * Given the rules for eoc (end-of-component, see AbstractCompositeType.compare()), + * We can select: + * - = 'a' by using <'a'><0> + * - < 'a' by using <'a'><-1> + * - <= 'a' by using <'a'><1> + * - > 'a' by using <'a'><1> + * - >= 'a' by using <'a'><0> + */ + switch (op) + { + case LT: + out.write((byte) -1); + break; + case GT: + case LTE: + out.write((byte) 1); + break; + default: + out.write((byte) 0); + break; + } + return this; + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public Builder add(ByteBuffer bb) + { + if (current >= composite.types.size()) + throw new IllegalStateException("Composite column is already fully constructed"); + + try + { + ByteBufferUtil.writeWithShortLength(bb, out); + out.write((byte) 0); + return this; + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public int componentCount() + { + return current; + } + + public ByteBuffer build() + { + return ByteBuffer.wrap(baos.toByteArray()); + } + + public ByteBuffer buildAsEndOfRange() + { + if (current >= composite.types.size()) + throw new IllegalStateException("Composite column is already fully constructed"); + + if (current == 0) + return ByteBufferUtil.EMPTY_BYTE_BUFFER; + + ByteBuffer bb = build(); + bb.put(bb.remaining() - 1, (byte)1); + return bb; + } + + public Builder copy() + { + return new Builder(this); + } + } } diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java index d2df8a9102..c66d99f82e 100644 --- a/src/java/org/apache/cassandra/service/ClientState.java +++ b/src/java/org/apache/cassandra/service/ClientState.java @@ -20,6 +20,8 @@ package org.apache.cassandra.service; import java.util.*; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,6 +34,7 @@ import org.apache.cassandra.cql.CQLStatement; import org.apache.cassandra.db.Table; import org.apache.cassandra.thrift.AuthenticationException; import org.apache.cassandra.thrift.InvalidRequestException; +import org.apache.cassandra.utils.SemanticVersion; /** * A container for per-client, thread-local state that Avro/Thrift threads must hold. @@ -41,12 +44,14 @@ public class ClientState { private static final int MAX_CACHE_PREPARED = 10000; // Enough to keep buggy clients from OOM'ing us private static Logger logger = LoggerFactory.getLogger(ClientState.class); + public static final SemanticVersion DEFAULT_CQL_VERSION = org.apache.cassandra.cql.QueryProcessor.CQL_VERSION; // Current user for the session private AuthenticatedUser user; private String keyspace; // Reusable array for authorization private final List resource = new ArrayList(); + private SemanticVersion cqlVersion = DEFAULT_CQL_VERSION; // An LRU map of prepared statements private Map prepared = new LinkedHashMap(16, 0.75f, true) { @@ -55,6 +60,12 @@ public class ClientState } }; + private Map cql3Prepared = new LinkedHashMap(16, 0.75f, true) { + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHE_PREPARED; + } + }; + private long clock; /** @@ -69,7 +80,12 @@ public class ClientState { return prepared; } - + + public Map getCQL3Prepared() + { + return cql3Prepared; + } + public String getRawKeyspace() { return keyspace; @@ -129,6 +145,7 @@ public class ClientState keyspace = null; resourceClear(); prepared.clear(); + cql3Prepared.clear(); } /** @@ -219,4 +236,42 @@ public class ClientState clock = clock >= current ? clock + 1 : current; return clock; } + + public void setCQLVersion(String str) throws InvalidRequestException + { + SemanticVersion version; + try + { + version = new SemanticVersion(str); + } + catch (IllegalArgumentException e) + { + throw new InvalidRequestException(e.getMessage()); + } + + SemanticVersion cql = org.apache.cassandra.cql.QueryProcessor.CQL_VERSION; + SemanticVersion cql3 = org.apache.cassandra.cql3.QueryProcessor.CQL_VERSION; + + if (version.isSupportedBy(cql)) + cqlVersion = cql; + else if (version.isSupportedBy(cql3)) + cqlVersion = cql3; + else + throw new InvalidRequestException(String.format("Provided version %s is not supported by this server (supported: %s)", + version, + StringUtils.join(getCQLSupportedVersion(), ", "))); + } + + public SemanticVersion getCQLVersion() + { + return cqlVersion; + } + + public static SemanticVersion[] getCQLSupportedVersion() + { + SemanticVersion cql = org.apache.cassandra.cql.QueryProcessor.CQL_VERSION; + SemanticVersion cql3 = org.apache.cassandra.cql3.QueryProcessor.CQL_VERSION; + + return new SemanticVersion[]{ cql, cql3 }; + } } diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java index c6d03b1561..975b729e2b 100644 --- a/src/java/org/apache/cassandra/service/StorageService.java +++ b/src/java/org/apache/cassandra/service/StorageService.java @@ -413,6 +413,7 @@ public class StorageService implements IEndpointStateChangeSubscriber, StorageSe { logger_.info("Cassandra version: " + FBUtilities.getReleaseVersionString()); logger_.info("Thrift API version: " + Constants.VERSION); + logger_.info("CQL supported versions: " + StringUtils.join(ClientState.getCQLSupportedVersion(), ",") + " (default: " + ClientState.DEFAULT_CQL_VERSION + ")"); if (initialized) { diff --git a/src/java/org/apache/cassandra/thrift/CassandraServer.java b/src/java/org/apache/cassandra/thrift/CassandraServer.java index 8611307c96..b84c3ce4dc 100644 --- a/src/java/org/apache/cassandra/thrift/CassandraServer.java +++ b/src/java/org/apache/cassandra/thrift/CassandraServer.java @@ -1182,7 +1182,10 @@ public class CassandraServer implements Cassandra.Iface try { - return QueryProcessor.process(queryString, state()); + if (state().getCQLVersion().major == 2) + return QueryProcessor.process(queryString, state()); + else + return org.apache.cassandra.cql3.QueryProcessor.process(queryString, state()); } catch (RecognitionException e) { @@ -1201,7 +1204,10 @@ public class CassandraServer implements Cassandra.Iface try { - return QueryProcessor.prepare(queryString, state()); + if (state().getCQLVersion().major == 2) + return QueryProcessor.prepare(queryString, state()); + else + return org.apache.cassandra.cql3.QueryProcessor.prepare(queryString, state()); } catch (RecognitionException e) { @@ -1215,14 +1221,34 @@ public class CassandraServer implements Cassandra.Iface throws InvalidRequestException, UnavailableException, TimedOutException, SchemaDisagreementException, TException { if (logger.isDebugEnabled()) logger.debug("execute_prepared_cql_query"); - - CQLStatement statement = state().getPrepared().get(itemId); - if (statement == null) - throw new InvalidRequestException(String.format("Prepared query with ID %d not found", itemId)); - logger.trace("Retrieved prepared statement #{} with {} bind markers", itemId, state().getPrepared().size()); + if (state().getCQLVersion().major == 2) + { + CQLStatement statement = state().getPrepared().get(itemId); - return QueryProcessor.processPrepared(statement, state(), bindVariables); + if (statement == null) + throw new InvalidRequestException(String.format("Prepared query with ID %d not found", itemId)); + logger.trace("Retrieved prepared statement #{} with {} bind markers", itemId, statement.boundTerms); + + return QueryProcessor.processPrepared(statement, state(), bindVariables); + } + else + { + org.apache.cassandra.cql3.CQLStatement statement = state().getCQL3Prepared().get(itemId); + + if (statement == null) + throw new InvalidRequestException(String.format("Prepared query with ID %d not found", itemId)); + logger.trace("Retrieved prepared statement #{} with {} bind markers", itemId, statement.getBoundsTerms()); + + return org.apache.cassandra.cql3.QueryProcessor.processPrepared(statement, state(), bindVariables); + } + } + + public void set_cql_version(String version) throws InvalidRequestException + { + logger.debug("set_cql_version: " + version); + + state().setCQLVersion(version); } // main method moved to CassandraDaemon diff --git a/src/java/org/apache/cassandra/thrift/ThriftValidation.java b/src/java/org/apache/cassandra/thrift/ThriftValidation.java index c09aeca742..a4e32b1560 100644 --- a/src/java/org/apache/cassandra/thrift/ThriftValidation.java +++ b/src/java/org/apache/cassandra/thrift/ThriftValidation.java @@ -617,10 +617,6 @@ public class ThriftValidation if (cf_def.column_metadata == null) return; - AbstractType comparator = cfType == ColumnFamilyType.Standard - ? TypeParser.parse(cf_def.comparator_type) - : TypeParser.parse(cf_def.subcomparator_type); - if (cf_def.key_alias != null) { // check if any of the columns has name equal to the cf.key_alias @@ -642,6 +638,8 @@ public class ThriftValidation indexNames.add(cd.getIndexName()); } + AbstractType comparator = CFMetaData.getColumnDefinitionComparator(cf_def); + for (ColumnDef c : cf_def.column_metadata) { TypeParser.parse(c.validation_class); @@ -653,7 +651,7 @@ public class ThriftValidation catch (MarshalException e) { throw new InvalidRequestException(String.format("Column name %s is not valid for comparator %s", - ByteBufferUtil.bytesToHex(c.name), cf_def.comparator_type)); + ByteBufferUtil.bytesToHex(c.name), comparator)); } if (c.index_type == null) diff --git a/src/java/org/apache/cassandra/utils/SemanticVersion.java b/src/java/org/apache/cassandra/utils/SemanticVersion.java new file mode 100644 index 0000000000..99b2acaacd --- /dev/null +++ b/src/java/org/apache/cassandra/utils/SemanticVersion.java @@ -0,0 +1,231 @@ +/* + * 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.utils; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.Arrays; + +import org.apache.commons.lang.StringUtils; +import com.google.common.base.Objects; + +/** + * Implements semantic versioning as defined at http://semver.org/. + * + * Note: The following code uses a slight variation from the document above in + * that it doesn't allow dashes in pre-release and build identifier. + */ +public class SemanticVersion implements Comparable +{ + private static final String VERSION_REGEXP = "(\\d+)\\.(\\d+)\\.(\\d+)(\\-[.\\w]+)?(\\+[.\\w]+)?"; + private static final Pattern pattern = Pattern.compile(VERSION_REGEXP); + + public final int major; + public final int minor; + public final int patch; + + private final String[] preRelease; + private final String[] build; + + private SemanticVersion(int major, int minor, int patch, String[] preRelease, String[] build) + { + this.major = major; + this.minor = minor; + this.patch = patch; + this.preRelease = preRelease; + this.build = build; + } + + /** + * Parse a semantic version from a string. + * + * @param version the string to parse + * @throws IllegalArgumentException if the provided string does not + * represent a semantic version + */ + public SemanticVersion(String version) + { + Matcher matcher = pattern.matcher(version); + if (!matcher.matches()) + throw new IllegalArgumentException("Invalid version value: " + version + " (see http://semver.org/ for details)"); + + try + { + this.major = Integer.valueOf(matcher.group(1)); + this.minor = Integer.valueOf(matcher.group(2)); + this.patch = Integer.valueOf(matcher.group(3)); + + String pr = matcher.group(4); + String bld = matcher.group(5); + + this.preRelease = pr == null || pr.isEmpty() ? null : parseIdentifiers(version, pr); + this.build = bld == null || bld.isEmpty() ? null : parseIdentifiers(version, bld); + + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("Invalid version value: " + version + " (see http://semver.org/ for details)"); + } + } + + private static String[] parseIdentifiers(String version, String str) + { + // Drop initial - or + + str = str.substring(1); + String[] parts = str.split("\\."); + for (String part : parts) + { + if (!part.matches("\\w+")) + throw new IllegalArgumentException("Invalid version value: " + version + " (see http://semver.org/ for details)"); + } + return parts; + } + + public int compareTo(SemanticVersion other) + { + if (major < other.major) + return -1; + if (major > other.major) + return 1; + + if (minor < other.minor) + return -1; + if (minor > other.minor) + return 1; + + if (patch < other.patch) + return -1; + if (patch > other.patch) + return 1; + + int c = compareIdentifiers(preRelease, other.preRelease, 1); + if (c != 0) + return c; + + return compareIdentifiers(build, other.build, -1); + } + + /** + * Returns a version that is backward compatible with this version amongst a list + * of provided version, or null if none can be found. + * + * For instance: + * "2.0.0".findSupportingVersion("2.0.0", "3.0.0") == "2.0.0" + * "2.0.0".findSupportingVersion("2.1.3", "3.0.0") == "2.1.3" + * "2.0.0".findSupportingVersion("3.0.0") == null + * "2.0.3".findSupportingVersion("2.0.0") == "2.0.0" + * "2.1.0".findSupportingVersion("2.0.0") == null + */ + public SemanticVersion findSupportingVersion(SemanticVersion... versions) + { + for (SemanticVersion version : versions) + { + if (isSupportedBy(version)) + return version; + } + return null; + } + + public boolean isSupportedBy(SemanticVersion version) + { + return major == version.major && minor <= version.minor; + } + + private static int compareIdentifiers(String[] ids1, String[] ids2, int defaultPred) + { + if (ids1 == null) + return ids2 == null ? 0 : defaultPred; + else if (ids2 == null) + return -defaultPred; + + int min = Math.min(ids1.length, ids2.length); + for (int i = 0; i < min; i++) + { + Integer i1 = tryParseInt(ids1[i]); + Integer i2 = tryParseInt(ids2[i]); + + if (i1 != null) + { + // integer have precedence + if (i2 == null || i1 < i2) + return -1; + else if (i1 > i2) + return 1; + } + else + { + // integer have precedence + if (i2 != null) + return 1; + + int c = ids1[i].compareTo(ids2[i]); + if (c != 0) + return c; + } + } + + if (ids1.length < ids2.length) + return -1; + if (ids1.length > ids2.length) + return 1; + return 0; + } + + private static Integer tryParseInt(String str) + { + try + { + return Integer.valueOf(str); + } + catch (NumberFormatException e) + { + return null; + } + } + + @Override + public boolean equals(Object o) + { + if(!(o instanceof SemanticVersion)) + return false; + SemanticVersion that = (SemanticVersion)o; + return major == that.major + && minor == that.minor + && patch == that.patch + && Arrays.equals(preRelease, that.preRelease) + && Arrays.equals(build, that.build); + } + + @Override + public int hashCode() + { + return Objects.hashCode(major, minor, patch, preRelease, build); + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(major).append('.').append(minor).append('.').append(patch); + if (preRelease != null) + sb.append('-').append(StringUtils.join(preRelease, ".")); + if (build != null) + sb.append('+').append(StringUtils.join(build, ".")); + return sb.toString(); + } +} diff --git a/test/unit/org/apache/cassandra/utils/SemanticVersionTest.java b/test/unit/org/apache/cassandra/utils/SemanticVersionTest.java new file mode 100644 index 0000000000..6d03d3f582 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/SemanticVersionTest.java @@ -0,0 +1,105 @@ +/* + * 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.utils; + +import org.junit.Test; + +public class SemanticVersionTest +{ + @Test + public void testParsing() + { + SemanticVersion version; + + version = new SemanticVersion("1.2.3"); + assert version.major == 1 && version.minor == 2 && version.patch == 3; + + version = new SemanticVersion("1.2.3-foo.2+Bar"); + assert version.major == 1 && version.minor == 2 && version.patch == 3; + } + + @Test + public void testComparison() + { + SemanticVersion v1, v2; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.2.4"); + assert v1.compareTo(v2) == -1; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.2.3"); + assert v1.compareTo(v2) == 0; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("2.0.0"); + assert v1.compareTo(v2) == -1; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.2.3-alpha"); + assert v1.compareTo(v2) == 1; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.2.3+foo"); + assert v1.compareTo(v2) == -1; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.2.3-alpha+foo"); + assert v1.compareTo(v2) == 1; + } + + @Test + public void testIsSupportedBy() + { + SemanticVersion v1, v2; + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.2.4"); + assert v1.isSupportedBy(v2); + assert v2.isSupportedBy(v1); + + v1 = new SemanticVersion("1.2.3"); + v2 = new SemanticVersion("1.3.3"); + assert v1.isSupportedBy(v2); + assert !v2.isSupportedBy(v1); + + v1 = new SemanticVersion("2.2.3"); + v2 = new SemanticVersion("1.3.3"); + assert !v1.isSupportedBy(v2); + assert !v2.isSupportedBy(v1); + } + + @Test + public void testInvalid() + { + assertThrows("1.0"); + assertThrows("1.0.0a"); + assertThrows("1.a.4"); + assertThrows("1.0.0-foo&"); + } + + private static void assertThrows(String str) + { + try + { + new SemanticVersion(str); + assert false; + } + catch (IllegalArgumentException e) {} + } +}