Distinguish between null and unset in the native protocol (v4) patch by odpeer; reviewed by blerer for CASSANDRA-7304
Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/48f64468 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/48f64468 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/48f64468 Branch: refs/heads/trunk Commit: 48f644686b48357354f16c74b02b6d2c450a8c2d Parents: bf51f24 Author: Oded Peer <peer.o...@gmail.com> Authored: Mon Apr 20 15:28:09 2015 +0200 Committer: Sylvain Lebresne <sylv...@datastax.com> Committed: Mon Apr 20 15:30:19 2015 +0200 ---------------------------------------------------------------------- CHANGES.txt | 1 + NEWS.txt | 12 ++ doc/native_protocol_v4.spec | 9 +- .../org/apache/cassandra/cql3/Attributes.java | 7 ++ .../apache/cassandra/cql3/ColumnCondition.java | 12 +- .../org/apache/cassandra/cql3/Constants.java | 19 +++- src/java/org/apache/cassandra/cql3/Lists.java | 43 ++++--- src/java/org/apache/cassandra/cql3/Maps.java | 43 +++++-- .../org/apache/cassandra/cql3/QueryOptions.java | 4 +- .../apache/cassandra/cql3/QueryProcessor.java | 3 + src/java/org/apache/cassandra/cql3/Sets.java | 38 ++++--- src/java/org/apache/cassandra/cql3/Tuples.java | 8 ++ .../org/apache/cassandra/cql3/UserTypes.java | 4 + .../cassandra/cql3/functions/FunctionCall.java | 7 +- .../cql3/restrictions/AbstractRestriction.java | 2 + .../restrictions/SingleColumnRestriction.java | 20 +++- .../cql3/statements/ModificationStatement.java | 8 +- .../cql3/statements/RequestValidations.java | 17 +++ .../cql3/statements/SelectStatement.java | 5 +- .../db/composites/CompositesBuilder.java | 23 +++- .../apache/cassandra/db/marshal/UserType.java | 5 + .../org/apache/cassandra/transport/CBUtil.java | 27 ++++- .../transport/messages/BatchMessage.java | 2 +- .../transport/messages/ExecuteMessage.java | 2 +- .../apache/cassandra/utils/ByteBufferUtil.java | 3 + .../org/apache/cassandra/cql3/CQLTester.java | 11 ++ .../apache/cassandra/cql3/CollectionsTest.java | 100 +++++++++++++++++ .../cassandra/cql3/ColumnConditionTest.java | 7 ++ .../cassandra/cql3/ContainsRelationTest.java | 12 ++ .../apache/cassandra/cql3/ModificationTest.java | 112 +++++++++++++++++++ .../cassandra/cql3/MultiColumnRelationTest.java | 22 ++++ .../cql3/SingleColumnRelationTest.java | 47 +++++++- .../apache/cassandra/cql3/TupleTypeTest.java | 13 +++ .../apache/cassandra/cql3/UserTypesTest.java | 15 +++ 34 files changed, 594 insertions(+), 69 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index 3dbd42f..d1d6dea 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 3.0 + * Distinguish between null and unset in protocol v4 (CASSANDRA-7304) * Add user/role permissions for user-defined functions (CASSANDRA-7557) * Allow cassandra config to be updated to restart daemon without unloading classes (CASSANDRA-9046) * Don't initialize compaction writer before checking if iter is empty (CASSANDRA-9117) http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/NEWS.txt ---------------------------------------------------------------------- diff --git a/NEWS.txt b/NEWS.txt index 7db07f0..03008de 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -58,6 +58,18 @@ New features - The node now keeps up when streaming is failed during bootstrapping. You can use new `nodetool bootstrap resume` command to continue streaming after resolving an issue. + - Protocol version 4 specifies that bind variables do not require having a + value when executing a statement. Bind variables without a value are + called 'unset'. The 'unset' bind variable is serialized as the int + value '-2' without following bytes. + In an EXECUTE or BATCH request an unset bind value does not modify the value and + does not create a tombstone, an unset bind ttl is treated as 'unlimited', + an unset bind timestamp is treated as 'now', an unset bind counter operation + does not change the counter value. + Unset tuple field, UDT field and map key are not allowed. + In a QUERY request an unset limit is treated as 'unlimited'. + Unset WHERE clauses with unset partition column, clustering column + or index column are not allowed. Upgrading http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/doc/native_protocol_v4.spec ---------------------------------------------------------------------- diff --git a/doc/native_protocol_v4.spec b/doc/native_protocol_v4.spec index ac57749..99483e3 100644 --- a/doc/native_protocol_v4.spec +++ b/doc/native_protocol_v4.spec @@ -206,6 +206,11 @@ Table of Contents [string list] A [short] n, followed by n [string]. [bytes] A [int] n, followed by n bytes if n >= 0. If n < 0, no byte should follow and the value represented is `null`. + [value] A [int] n, followed by n bytes if n >= 0. + If n == -1 no byte should follow and the value represented is `null`. + If n == -2 no byte should follow and the value represented is + `not set` not resulting in any change to the existing value. + n < -2 is an invalid value and results in an error. [short bytes] A [short] n, followed by n bytes if n >= 0. [option] A pair of <id><value> where <id> is a [short] representing @@ -306,7 +311,7 @@ Table of Contents in particular influence what the remainder of the message contains. A flag is set if the bit corresponding to its `mask` is set. Supported flags are, given there mask: - 0x01: Values. In that case, a [short] <n> followed by <n> [bytes] + 0x01: Values. In that case, a [short] <n> followed by <n> [value] values are provided. Those value are used for bound variables in the query. Optionally, if the 0x40 flag is present, each value will be preceded by a [string] name, representing the name of @@ -418,7 +423,7 @@ Table of Contents - <n> is a [short] indicating the number (possibly 0) of following values. - <name_i> is the optional name of the following <value_i>. It must be present if and only if the 0x40 flag is provided for the batch. - - <value_i> is the [bytes] to use for bound variable i (of bound variable <name_i> + - <value_i> is the [value] to use for bound variable i (of bound variable <name_i> if the 0x40 flag is used). - <consistency> is the [consistency] level for the operation. - <serial_consistency> is only present if the 0x10 flag is set. In that case, http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/Attributes.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Attributes.java b/src/java/org/apache/cassandra/cql3/Attributes.java index 4136ec5..f3d298b 100644 --- a/src/java/org/apache/cassandra/cql3/Attributes.java +++ b/src/java/org/apache/cassandra/cql3/Attributes.java @@ -28,6 +28,7 @@ import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.LongType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.ByteBufferUtil; /** * Utility class for the Parser to gather attributes for modification @@ -86,6 +87,9 @@ public class Attributes if (tval == null) throw new InvalidRequestException("Invalid null value of timestamp"); + if (tval == ByteBufferUtil.UNSET_BYTE_BUFFER) + return now; + try { LongType.instance.validate(tval); @@ -107,6 +111,9 @@ public class Attributes if (tval == null) throw new InvalidRequestException("Invalid null value of TTL"); + if (tval == ByteBufferUtil.UNSET_BYTE_BUFFER) // treat as unlimited + return 0; + try { Int32Type.instance.validate(tval); http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/ColumnCondition.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/ColumnCondition.java b/src/java/org/apache/cassandra/cql3/ColumnCondition.java index 921a073..acebfe7 100644 --- a/src/java/org/apache/cassandra/cql3/ColumnCondition.java +++ b/src/java/org/apache/cassandra/cql3/ColumnCondition.java @@ -189,6 +189,8 @@ public class ColumnCondition /** Returns true if the operator is satisfied (i.e. "value operator otherValue == true"), false otherwise. */ protected boolean compareWithOperator(Operator operator, AbstractType<?> type, ByteBuffer value, ByteBuffer otherValue) throws InvalidRequestException { + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException("Invalid 'unset' value in condition"); if (value == null) { switch (operator) @@ -278,7 +280,7 @@ public class ColumnCondition assert !(column.type instanceof CollectionType) && condition.collectionElement == null; assert condition.operator == Operator.IN; if (condition.inValues == null) - this.inValues = ((Lists.Marker) condition.value).bind(options).getElements(); + this.inValues = ((Lists.Value) condition.value.bind(options)).getElements(); else { this.inValues = new ArrayList<>(condition.inValues.size()); @@ -389,7 +391,7 @@ public class ColumnCondition this.collectionElement = condition.collectionElement.bindAndGet(options); if (condition.inValues == null) - this.inValues = ((Lists.Marker) condition.value).bind(options).getElements(); + this.inValues = ((Lists.Value) condition.value.bind(options)).getElements(); else { this.inValues = new ArrayList<>(condition.inValues.size()); @@ -656,7 +658,7 @@ public class ColumnCondition if (column.type instanceof ListType) { ListType deserializer = ListType.getInstance(collectionType.valueComparator(), false); - for (ByteBuffer buffer : inValuesMarker.bind(options).elements) + for (ByteBuffer buffer : ((Lists.Value)inValuesMarker.bind(options)).elements) { if (buffer == null) this.inValues.add(null); @@ -667,7 +669,7 @@ public class ColumnCondition else if (column.type instanceof MapType) { MapType deserializer = MapType.getInstance(collectionType.nameComparator(), collectionType.valueComparator(), false); - for (ByteBuffer buffer : inValuesMarker.bind(options).elements) + for (ByteBuffer buffer : ((Lists.Value)inValuesMarker.bind(options)).elements) { if (buffer == null) this.inValues.add(null); @@ -678,7 +680,7 @@ public class ColumnCondition else if (column.type instanceof SetType) { SetType deserializer = SetType.getInstance(collectionType.valueComparator(), false); - for (ByteBuffer buffer : inValuesMarker.bind(options).elements) + for (ByteBuffer buffer : ((Lists.Value)inValuesMarker.bind(options)).elements) { if (buffer == null) this.inValues.add(null); http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/Constants.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Constants.java b/src/java/org/apache/cassandra/cql3/Constants.java index 5f48160..8619b24 100644 --- a/src/java/org/apache/cassandra/cql3/Constants.java +++ b/src/java/org/apache/cassandra/cql3/Constants.java @@ -21,7 +21,6 @@ import java.nio.ByteBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.db.*; import org.apache.cassandra.db.composites.CellName; @@ -47,6 +46,8 @@ public abstract class Constants STRING, INTEGER, UUID, FLOAT, DATE, TIME, BOOLEAN, HEX; } + public static final Value UNSET_VALUE = new Value(ByteBufferUtil.UNSET_BYTE_BUFFER); + public static final Term.Raw NULL_LITERAL = new Term.Raw() { public Term prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException @@ -288,7 +289,7 @@ public abstract class Constants try { ByteBuffer value = options.getValues().get(bindIndex); - if (value != null) + if (value != null && value != ByteBufferUtil.UNSET_BYTE_BUFFER) receiver.type.validate(value); return value; } @@ -301,7 +302,11 @@ public abstract class Constants public Value bind(QueryOptions options) throws InvalidRequestException { ByteBuffer bytes = bindAndGet(options); - return bytes == null ? null : new Constants.Value(bytes); + if (bytes == null) + return null; + if (bytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + return Constants.UNSET_VALUE; + return new Constants.Value(bytes); } } @@ -316,7 +321,8 @@ public abstract class Constants { CellName cname = cf.getComparator().create(prefix, column); ByteBuffer value = t.bindAndGet(params.options); - cf.addColumn(value == null ? params.makeTombstone(cname) : params.makeColumn(cname, value)); + if (value != ByteBufferUtil.UNSET_BYTE_BUFFER) // use reference equality and not object equality + cf.addColumn(value == null ? params.makeTombstone(cname) : params.makeColumn(cname, value)); } } @@ -332,6 +338,9 @@ public abstract class Constants ByteBuffer bytes = t.bindAndGet(params.options); if (bytes == null) throw new InvalidRequestException("Invalid null value for counter increment"); + if (bytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + return; + long increment = ByteBufferUtil.toLong(bytes); CellName cname = cf.getComparator().create(prefix, column); cf.addColumn(params.makeCounter(cname, increment)); @@ -350,6 +359,8 @@ public abstract class Constants ByteBuffer bytes = t.bindAndGet(params.options); if (bytes == null) throw new InvalidRequestException("Invalid null value for counter increment"); + if (bytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + return; long increment = ByteBufferUtil.toLong(bytes); if (increment == Long.MIN_VALUE) http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/Lists.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Lists.java b/src/java/org/apache/cassandra/cql3/Lists.java index 8b8375d..d695d8c 100644 --- a/src/java/org/apache/cassandra/cql3/Lists.java +++ b/src/java/org/apache/cassandra/cql3/Lists.java @@ -17,6 +17,8 @@ */ package org.apache.cassandra.cql3; +import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; + import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -135,8 +137,8 @@ public abstract class Lists { // Collections have this small hack that validate cannot be called on a serialized object, // but compose does the validation (so we're fine). - List<?> l = (List<?>)type.getSerializer().deserializeForNativeProtocol(value, version); - List<ByteBuffer> elements = new ArrayList<ByteBuffer>(l.size()); + List<?> l = type.getSerializer().deserializeForNativeProtocol(value, version); + List<ByteBuffer> elements = new ArrayList<>(l.size()); for (Object element : l) // elements can be null in lists that represent a set of IN values elements.add(element == null ? null : type.getElementsType().decompose(element)); @@ -199,7 +201,7 @@ public abstract class Lists { } - public Value bind(QueryOptions options) throws InvalidRequestException + public Terminal bind(QueryOptions options) throws InvalidRequestException { List<ByteBuffer> buffers = new ArrayList<ByteBuffer>(elements.size()); for (Term t : elements) @@ -208,6 +210,8 @@ public abstract class Lists if (bytes == null) throw new InvalidRequestException("null is not supported inside collections"); + if (bytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; // We don't support value > 64K because the serialization format encode the length as an unsigned short. if (bytes.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) @@ -237,10 +241,14 @@ public abstract class Lists assert receiver.type instanceof ListType; } - public Value bind(QueryOptions options) throws InvalidRequestException + public Terminal bind(QueryOptions options) throws InvalidRequestException { ByteBuffer value = options.getValues().get(bindIndex); - return value == null ? null : Value.fromSerialized(value, (ListType)receiver.type, options.getProtocolVersion()); + if (value == null) + return null; + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; + return Value.fromSerialized(value, (ListType)receiver.type, options.getProtocolVersion()); } } @@ -256,7 +264,7 @@ public abstract class Lists { // Our reference time (1 jan 2010, 00:00:00) in milliseconds. private static final long REFERENCE_TIME = 1262304000000L; - private static final AtomicReference<PrecisionTime> last = new AtomicReference<PrecisionTime>(new PrecisionTime(Long.MAX_VALUE, 0)); + private static final AtomicReference<PrecisionTime> last = new AtomicReference<>(new PrecisionTime(Long.MAX_VALUE, 0)); public final long millis; public final int nanos; @@ -293,13 +301,15 @@ public abstract class Lists public void execute(ByteBuffer rowKey, ColumnFamily cf, Composite prefix, UpdateParameters params) throws InvalidRequestException { - if (column.type.isMultiCell()) + Term.Terminal value = t.bind(params.options); + if (column.type.isMultiCell() && value != UNSET_VALUE) { // delete + append CellName name = cf.getComparator().create(prefix, column); cf.addAtom(params.makeTombstoneForOverwrite(name.slice())); } - Appender.doAppend(t, cf, prefix, column, params); + if (value != UNSET_VALUE) + Appender.doAppend(cf, prefix, column, params, value); } } @@ -336,6 +346,8 @@ public abstract class Lists if (index == null) throw new InvalidRequestException("Invalid null value for list index"); + if (index == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException("Invalid unset value for list index"); List<Cell> existingList = params.getPrefetchedList(rowKey, column.name); int idx = ByteBufferUtil.toInt(index); @@ -349,7 +361,7 @@ public abstract class Lists { cf.addColumn(params.makeTombstone(elementName)); } - else + else if (value != ByteBufferUtil.UNSET_BYTE_BUFFER) { // We don't support value > 64K because the serialization format encode the length as an unsigned short. if (value.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) @@ -372,12 +384,13 @@ public abstract class Lists public void execute(ByteBuffer rowKey, ColumnFamily cf, Composite prefix, UpdateParameters params) throws InvalidRequestException { assert column.type.isMultiCell() : "Attempted to append to a frozen list"; - doAppend(t, cf, prefix, column, params); + Term.Terminal value = t.bind(params.options); + if (value != UNSET_VALUE) + doAppend(cf, prefix, column, params, value); } - static void doAppend(Term t, ColumnFamily cf, Composite prefix, ColumnDefinition column, UpdateParameters params) throws InvalidRequestException + static void doAppend(ColumnFamily cf, Composite prefix, ColumnDefinition column, UpdateParameters params, Term.Terminal value) throws InvalidRequestException { - Term.Terminal value = t.bind(params.options); if (column.type.isMultiCell()) { // If we append null, do nothing. Note that for Setter, we've @@ -414,7 +427,7 @@ public abstract class Lists { assert column.type.isMultiCell() : "Attempted to prepend to a frozen list"; Term.Terminal value = t.bind(params.options); - if (value == null) + if (value == null || value == UNSET_VALUE) return; long time = PrecisionTime.REFERENCE_TIME - (System.currentTimeMillis() - PrecisionTime.REFERENCE_TIME); @@ -454,7 +467,7 @@ public abstract class Lists if (existingList.isEmpty()) return; - if (value == null) + if (value == null || value == UNSET_VALUE) return; // Note: below, we will call 'contains' on this toDiscard list for each element of existingList. @@ -489,6 +502,8 @@ public abstract class Lists Term.Terminal index = t.bind(params.options); if (index == null) throw new InvalidRequestException("Invalid null value for list index"); + if (index == Constants.UNSET_VALUE) + return; List<Cell> existingList = params.getPrefetchedList(rowKey, column.name); int idx = ByteBufferUtil.toInt(index.get(params.options.getProtocolVersion())); http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/Maps.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Maps.java b/src/java/org/apache/cassandra/cql3/Maps.java index 6c7cfa6..5bb3a48 100644 --- a/src/java/org/apache/cassandra/cql3/Maps.java +++ b/src/java/org/apache/cassandra/cql3/Maps.java @@ -17,6 +17,8 @@ */ package org.apache.cassandra.cql3; +import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; + import java.nio.ByteBuffer; import java.util.*; @@ -32,6 +34,7 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.CollectionSerializer; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.transport.Server; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; @@ -67,7 +70,7 @@ public abstract class Maps ColumnSpecification keySpec = Maps.keySpecOf(receiver); ColumnSpecification valueSpec = Maps.valueSpecOf(receiver); - Map<Term, Term> values = new HashMap<Term, Term>(entries.size()); + Map<Term, Term> values = new HashMap<>(entries.size()); boolean allTerminal = true; for (Pair<Term.Raw, Term.Raw> entry : entries) { @@ -157,8 +160,8 @@ public abstract class Maps { // Collections have this small hack that validate cannot be called on a serialized object, // but compose does the validation (so we're fine). - Map<?, ?> m = (Map<?, ?>)type.getSerializer().deserializeForNativeProtocol(value, version); - Map<ByteBuffer, ByteBuffer> map = new LinkedHashMap<ByteBuffer, ByteBuffer>(m.size()); + Map<?, ?> m = type.getSerializer().deserializeForNativeProtocol(value, version); + Map<ByteBuffer, ByteBuffer> map = new LinkedHashMap<>(m.size()); for (Map.Entry<?, ?> entry : m.entrySet()) map.put(type.getKeysType().decompose(entry.getKey()), type.getValuesType().decompose(entry.getValue())); return new Value(map); @@ -222,7 +225,7 @@ public abstract class Maps { } - public Value bind(QueryOptions options) throws InvalidRequestException + public Terminal bind(QueryOptions options) throws InvalidRequestException { Map<ByteBuffer, ByteBuffer> buffers = new TreeMap<ByteBuffer, ByteBuffer>(comparator); for (Map.Entry<Term, Term> entry : elements.entrySet()) @@ -231,6 +234,8 @@ public abstract class Maps ByteBuffer keyBytes = entry.getKey().bindAndGet(options); if (keyBytes == null) throw new InvalidRequestException("null is not supported inside collections"); + if (keyBytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException("unset value is not supported for map keys"); if (keyBytes.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) throw new InvalidRequestException(String.format("Map key is too long. Map keys are limited to %d bytes but %d bytes keys provided", FBUtilities.MAX_UNSIGNED_SHORT, @@ -239,6 +244,9 @@ public abstract class Maps ByteBuffer valueBytes = entry.getValue().bindAndGet(options); if (valueBytes == null) throw new InvalidRequestException("null is not supported inside collections"); + if (valueBytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; + if (valueBytes.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) throw new InvalidRequestException(String.format("Map value is too long. Map values are limited to %d bytes but %d bytes value provided", FBUtilities.MAX_UNSIGNED_SHORT, @@ -264,10 +272,14 @@ public abstract class Maps assert receiver.type instanceof MapType; } - public Value bind(QueryOptions options) throws InvalidRequestException + public Terminal bind(QueryOptions options) throws InvalidRequestException { ByteBuffer value = options.getValues().get(bindIndex); - return value == null ? null : Value.fromSerialized(value, (MapType)receiver.type, options.getProtocolVersion()); + if (value == null) + return null; + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; + return Value.fromSerialized(value, (MapType)receiver.type, options.getProtocolVersion()); } } @@ -280,13 +292,15 @@ public abstract class Maps public void execute(ByteBuffer rowKey, ColumnFamily cf, Composite prefix, UpdateParameters params) throws InvalidRequestException { - if (column.type.isMultiCell()) + Term.Terminal value = t.bind(params.options); + if (column.type.isMultiCell() && value != UNSET_VALUE) { // delete + put CellName name = cf.getComparator().create(prefix, column); cf.addAtom(params.makeTombstoneForOverwrite(name.slice())); } - Putter.doPut(t, cf, prefix, column, params); + if (value != UNSET_VALUE) + Putter.doPut(cf, prefix, column, params, value); } } @@ -314,6 +328,8 @@ public abstract class Maps ByteBuffer value = t.bindAndGet(params.options); if (key == null) throw new InvalidRequestException("Invalid null map key"); + if (key == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException("Invalid unset map key"); CellName cellName = cf.getComparator().create(prefix, column, key); @@ -321,7 +337,7 @@ public abstract class Maps { cf.addColumn(params.makeTombstone(cellName)); } - else + else if (value != ByteBufferUtil.UNSET_BYTE_BUFFER) { // We don't support value > 64K because the serialization format encode the length as an unsigned short. if (value.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) @@ -344,12 +360,13 @@ public abstract class Maps public void execute(ByteBuffer rowKey, ColumnFamily cf, Composite prefix, UpdateParameters params) throws InvalidRequestException { assert column.type.isMultiCell() : "Attempted to add items to a frozen map"; - doPut(t, cf, prefix, column, params); + Term.Terminal value = t.bind(params.options); + if (value != UNSET_VALUE) + doPut(cf, prefix, column, params, value); } - static void doPut(Term t, ColumnFamily cf, Composite prefix, ColumnDefinition column, UpdateParameters params) throws InvalidRequestException + static void doPut(ColumnFamily cf, Composite prefix, ColumnDefinition column, UpdateParameters params, Term.Terminal value) throws InvalidRequestException { - Term.Terminal value = t.bind(params.options); if (column.type.isMultiCell()) { if (value == null) @@ -387,6 +404,8 @@ public abstract class Maps Term.Terminal key = t.bind(params.options); if (key == null) throw new InvalidRequestException("Invalid null map key"); + if (key == Constants.UNSET_VALUE) + throw new InvalidRequestException("Invalid unset map key"); CellName cellName = cf.getComparator().create(prefix, column, key.get(params.options.getProtocolVersion())); cf.addColumn(params.makeTombstone(cellName)); http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/QueryOptions.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/QueryOptions.java b/src/java/org/apache/cassandra/cql3/QueryOptions.java index 98c2395..fb46b9b 100644 --- a/src/java/org/apache/cassandra/cql3/QueryOptions.java +++ b/src/java/org/apache/cassandra/cql3/QueryOptions.java @@ -312,13 +312,13 @@ public abstract class QueryOptions { if (flags.contains(Flag.NAMES_FOR_VALUES)) { - Pair<List<String>, List<ByteBuffer>> namesAndValues = CBUtil.readNameAndValueList(body); + Pair<List<String>, List<ByteBuffer>> namesAndValues = CBUtil.readNameAndValueList(body, version); names = namesAndValues.left; values = namesAndValues.right; } else { - values = CBUtil.readValueList(body); + values = CBUtil.readValueList(body, version); } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/QueryProcessor.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/QueryProcessor.java b/src/java/org/apache/cassandra/cql3/QueryProcessor.java index fcda13d..f494436 100644 --- a/src/java/org/apache/cassandra/cql3/QueryProcessor.java +++ b/src/java/org/apache/cassandra/cql3/QueryProcessor.java @@ -60,6 +60,7 @@ import org.apache.cassandra.service.pager.QueryPagers; import org.apache.cassandra.thrift.ThriftClientState; import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MD5Digest; import org.apache.cassandra.utils.SemanticVersion; @@ -192,6 +193,8 @@ public class QueryProcessor implements QueryHandler { throw new InvalidRequestException("Key may not be empty"); } + if (key == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException("Key may not be unset"); // check that key can be handled by FBUtilities.writeShortByteArray if (key.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/Sets.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Sets.java b/src/java/org/apache/cassandra/cql3/Sets.java index ce372f8..093f1dc 100644 --- a/src/java/org/apache/cassandra/cql3/Sets.java +++ b/src/java/org/apache/cassandra/cql3/Sets.java @@ -17,6 +17,8 @@ */ package org.apache.cassandra.cql3; +import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; + import java.nio.ByteBuffer; import java.util.*; @@ -68,7 +70,7 @@ public abstract class Sets return new Maps.Value(Collections.<ByteBuffer, ByteBuffer>emptyMap()); ColumnSpecification valueSpec = Sets.valueSpecOf(receiver); - Set<Term> values = new HashSet<Term>(elements.size()); + Set<Term> values = new HashSet<>(elements.size()); boolean allTerminal = true; for (Term.Raw rt : elements) { @@ -147,8 +149,8 @@ public abstract class Sets { // Collections have this small hack that validate cannot be called on a serialized object, // but compose does the validation (so we're fine). - Set<?> s = (Set<?>)type.getSerializer().deserializeForNativeProtocol(value, version); - SortedSet<ByteBuffer> elements = new TreeSet<ByteBuffer>(type.getElementsType()); + Set<?> s = type.getSerializer().deserializeForNativeProtocol(value, version); + SortedSet<ByteBuffer> elements = new TreeSet<>(type.getElementsType()); for (Object element : s) elements.add(type.getElementsType().decompose(element)); return new Value(elements); @@ -202,7 +204,7 @@ public abstract class Sets { } - public Value bind(QueryOptions options) throws InvalidRequestException + public Terminal bind(QueryOptions options) throws InvalidRequestException { SortedSet<ByteBuffer> buffers = new TreeSet<>(comparator); for (Term t : elements) @@ -211,6 +213,8 @@ public abstract class Sets if (bytes == null) throw new InvalidRequestException("null is not supported inside collections"); + if (bytes == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; // We don't support value > 64K because the serialization format encode the length as an unsigned short. if (bytes.remaining() > FBUtilities.MAX_UNSIGNED_SHORT) @@ -237,10 +241,14 @@ public abstract class Sets assert receiver.type instanceof SetType; } - public Value bind(QueryOptions options) throws InvalidRequestException + public Terminal bind(QueryOptions options) throws InvalidRequestException { ByteBuffer value = options.getValues().get(bindIndex); - return value == null ? null : Value.fromSerialized(value, (SetType)receiver.type, options.getProtocolVersion()); + if (value == null) + return null; + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; + return Value.fromSerialized(value, (SetType)receiver.type, options.getProtocolVersion()); } } @@ -253,13 +261,15 @@ public abstract class Sets public void execute(ByteBuffer rowKey, ColumnFamily cf, Composite prefix, UpdateParameters params) throws InvalidRequestException { - if (column.type.isMultiCell()) + Term.Terminal value = t.bind(params.options); + if (column.type.isMultiCell() && value != UNSET_VALUE) { // delete + add CellName name = cf.getComparator().create(prefix, column); cf.addAtom(params.makeTombstoneForOverwrite(name.slice())); } - Adder.doAdd(t, cf, prefix, column, params); + if (value != UNSET_VALUE) + Adder.doAdd(cf, prefix, column, params, value); } } @@ -273,13 +283,13 @@ public abstract class Sets public void execute(ByteBuffer rowKey, ColumnFamily cf, Composite prefix, UpdateParameters params) throws InvalidRequestException { assert column.type.isMultiCell() : "Attempted to add items to a frozen set"; - - doAdd(t, cf, prefix, column, params); + Term.Terminal value = t.bind(params.options); + if (value != UNSET_VALUE) + doAdd(cf, prefix, column, params, value); } - static void doAdd(Term t, ColumnFamily cf, Composite prefix, ColumnDefinition column, UpdateParameters params) throws InvalidRequestException + static void doAdd(ColumnFamily cf, Composite prefix, ColumnDefinition column, UpdateParameters params, Term.Terminal value) throws InvalidRequestException { - Term.Terminal value = t.bind(params.options); if (column.type.isMultiCell()) { if (value == null) @@ -287,6 +297,8 @@ public abstract class Sets for (ByteBuffer bb : ((Value) value).elements) { + if (bb == ByteBufferUtil.UNSET_BYTE_BUFFER) + continue; CellName cellName = cf.getComparator().create(prefix, column, bb); cf.addColumn(params.makeColumn(cellName, ByteBufferUtil.EMPTY_BYTE_BUFFER)); } @@ -316,7 +328,7 @@ public abstract class Sets assert column.type.isMultiCell() : "Attempted to remove items from a frozen set"; Term.Terminal value = t.bind(params.options); - if (value == null) + if (value == null || value == UNSET_VALUE) return; // This can be either a set or a single element http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/Tuples.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/Tuples.java b/src/java/org/apache/cassandra/cql3/Tuples.java index 967ce37..92ccbce 100644 --- a/src/java/org/apache/cassandra/cql3/Tuples.java +++ b/src/java/org/apache/cassandra/cql3/Tuples.java @@ -30,6 +30,7 @@ import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.ByteBufferUtil; /** * Static helper methods and classes for tuples. @@ -204,6 +205,9 @@ public class Tuples for (int i = 0; i < elements.size(); i++) { buffers[i] = elements.get(i).bindAndGet(options); + // Since A tuple value is always written in its entirety Cassandra can't preserve a pre-existing value by 'not setting' the new value. Reject the query. + if (buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException(String.format("Invalid unset value for tuple field number %d", i)); // Inside tuples, we must force the serialization of collections to v3 whatever protocol // version is in use since we're going to store directly that serialized value. if (version < 3 && type.type(i).isCollection()) @@ -387,6 +391,8 @@ public class Tuples public Value bind(QueryOptions options) throws InvalidRequestException { ByteBuffer value = options.getValues().get(bindIndex); + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException(String.format("Invalid unset value for tuple %s", receiver.name)); return value == null ? null : Value.fromSerialized(value, (TupleType)receiver.type); } } @@ -405,6 +411,8 @@ public class Tuples public InValue bind(QueryOptions options) throws InvalidRequestException { ByteBuffer value = options.getValues().get(bindIndex); + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException(String.format("Invalid unset value for %s", receiver.name)); return value == null ? null : InValue.fromSerialized(value, (ListType)receiver.type, options); } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/UserTypes.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/UserTypes.java b/src/java/org/apache/cassandra/cql3/UserTypes.java index f00a376..b6d8521 100644 --- a/src/java/org/apache/cassandra/cql3/UserTypes.java +++ b/src/java/org/apache/cassandra/cql3/UserTypes.java @@ -26,6 +26,7 @@ import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.transport.Server; +import org.apache.cassandra.utils.ByteBufferUtil; /** * Static helper methods and classes for user types. @@ -185,6 +186,9 @@ public abstract class UserTypes for (int i = 0; i < type.size(); i++) { buffers[i] = values.get(i).bindAndGet(options); + // Since A UDT value is always written in its entirety Cassandra can't preserve a pre-existing value by 'not setting' the new value. Reject the query. + if (buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw new InvalidRequestException(String.format("Invalid unset value for field '%s' of user defined type %s", type.fieldNameAsString(i), type.getNameAsString())); // Inside UDT values, we must force the serialization of collections to v3 whatever protocol // version is in use since we're going to store directly that serialized value. if (version < Server.VERSION_3 && type.fieldType(i).isCollection() && buffers[i] != null) http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java index d8efa7f..f42a093 100644 --- a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java +++ b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java @@ -24,6 +24,7 @@ import java.util.List; import com.google.common.collect.Iterables; import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.statements.RequestValidations; import org.apache.cassandra.db.marshal.*; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.MarshalException; @@ -65,7 +66,11 @@ public class FunctionCall extends Term.NonTerminal { List<ByteBuffer> buffers = new ArrayList<>(terms.size()); for (Term t : terms) - buffers.add(t.bindAndGet(options)); + { + ByteBuffer functionArg = t.bindAndGet(options); + RequestValidations.checkBindValueSet(functionArg, "Invalid unset value for argument in call to function %s", fun.name().name); + buffers.add(functionArg); + } return executeInternal(options.getProtocolVersion(), fun, buffers); } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java index b212f4d..d36162c 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/AbstractRestriction.java @@ -29,6 +29,7 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkBindValueSet; /** * Base class for <code>Restriction</code>s @@ -94,6 +95,7 @@ abstract class AbstractRestriction implements Restriction throws InvalidRequestException { checkNotNull(value, "Unsupported null value for indexed column %s", columnSpec.name); + checkBindValueSet(value, "Unsupported unset value for indexed column %s", columnSpec.name); checkFalse(value.remaining() > 0xFFFF, "Index expression values may not be larger than 64K"); return value; } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java index ad4893f..69ef5d2 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java @@ -23,9 +23,12 @@ import java.util.*; import com.google.common.collect.Iterables; import org.apache.cassandra.config.ColumnDefinition; + import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.Term.Terminal; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.statements.Bound; + import org.apache.cassandra.db.IndexExpression; import org.apache.cassandra.db.composites.CompositesBuilder; import org.apache.cassandra.db.index.SecondaryIndex; @@ -33,9 +36,12 @@ import org.apache.cassandra.db.index.SecondaryIndexManager; import org.apache.cassandra.db.marshal.CompositeType; import org.apache.cassandra.exceptions.InvalidRequestException; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; + import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkBindValueSet; public abstract class SingleColumnRestriction extends AbstractRestriction { @@ -136,6 +142,7 @@ public abstract class SingleColumnRestriction extends AbstractRestriction { builder.addElementToAll(value.bindAndGet(options)); checkFalse(builder.containsNull(), "Invalid null value in condition for column %s", columnDef.name); + checkFalse(builder.containsUnset(), "Invalid unset value for column %s", columnDef.name); return builder; } @@ -182,6 +189,7 @@ public abstract class SingleColumnRestriction extends AbstractRestriction { builder.addEachElementToAll(getValues(options)); checkFalse(builder.containsNull(), "Invalid null value in condition for column %s", columnDef.name); + checkFalse(builder.containsUnset(), "Invalid unset value for column %s", columnDef.name); return builder; } @@ -269,9 +277,10 @@ public abstract class SingleColumnRestriction extends AbstractRestriction @Override protected List<ByteBuffer> getValues(QueryOptions options) throws InvalidRequestException { - Term.MultiItemTerminal lval = (Term.MultiItemTerminal) marker.bind(options); - if (lval == null) - throw new InvalidRequestException("Invalid null value for IN restriction"); + Terminal term = marker.bind(options); + checkNotNull(term, "Invalid null value for column %s", columnDef.name); + checkFalse(term == Constants.UNSET_VALUE, "Invalid unset value for column %s", columnDef.name); + Term.MultiItemTerminal lval = (Term.MultiItemTerminal) term; return lval.getElements(); } @@ -326,7 +335,10 @@ public abstract class SingleColumnRestriction extends AbstractRestriction @Override public CompositesBuilder appendBoundTo(CompositesBuilder builder, Bound bound, QueryOptions options) { - return builder.addElementToAll(slice.bound(bound).bindAndGet(options)); + ByteBuffer value = slice.bound(bound).bindAndGet(options); + checkBindValueSet(value, "Invalid unset value for column %s", columnDef.name); + return builder.addElementToAll(value); + } @Override http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 657e6e0..e7a1a20 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -641,10 +641,12 @@ public abstract class ModificationStatement implements CQLStatement for (IMutation mutation : getMutations(options, true, queryState.getTimestamp())) { - // We don't use counters internally. - assert mutation instanceof Mutation; + assert mutation instanceof Mutation || mutation instanceof CounterMutation; - ((Mutation) mutation).apply(); + if (mutation instanceof Mutation) + ((Mutation) mutation).apply(); + else if (mutation instanceof CounterMutation) + ((CounterMutation) mutation).apply(); } return null; } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/statements/RequestValidations.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/RequestValidations.java b/src/java/org/apache/cassandra/cql3/statements/RequestValidations.java index c822325..fc07878 100644 --- a/src/java/org/apache/cassandra/cql3/statements/RequestValidations.java +++ b/src/java/org/apache/cassandra/cql3/statements/RequestValidations.java @@ -17,11 +17,13 @@ */ package org.apache.cassandra.cql3.statements; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.commons.lang3.ArrayUtils.EMPTY_OBJECT_ARRAY; @@ -140,6 +142,21 @@ public final class RequestValidations } /** + * Checks that the specified bind marker value is set to a meaningful value. + * If it is not a <code>InvalidRequestException</code> will be thrown. + * + * @param b the <code>ByteBuffer</code> to test + * @param messageTemplate the template used to build the error message + * @param messageArgs the message arguments + * @throws InvalidRequestException if the specified bind marker value is not set to a meaningful value. + */ + public static void checkBindValueSet(ByteBuffer b, String messageTemplate, Object... messageArgs) + throws InvalidRequestException + { + checkTrue(b != ByteBufferUtil.UNSET_BYTE_BUFFER, messageTemplate, messageArgs); + } + + /** * Checks that the specified object is <code>null</code>. * If it is not an <code>InvalidRequestException</code> will be throws. * http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index c7791fb..8f7f75e 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -58,6 +58,7 @@ import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; +import static org.apache.cassandra.utils.ByteBufferUtil.UNSET_BYTE_BUFFER; /** * Encapsulates a completely parsed SELECT query, including the target @@ -466,7 +467,9 @@ public class SelectStatement implements CQLStatement if (limit != null) { ByteBuffer b = checkNotNull(limit.bindAndGet(options), "Invalid null value of limit"); - + // treat UNSET limit value as 'unlimited' + if (b == UNSET_BYTE_BUFFER) + return Integer.MAX_VALUE; try { Int32Type.instance.validate(b); http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java b/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java index 48bc802..25a510f 100644 --- a/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java +++ b/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.util.*; import org.apache.cassandra.db.composites.Composite.EOC; +import org.apache.cassandra.utils.ByteBufferUtil; import static java.util.Collections.singletonList; @@ -59,6 +60,11 @@ public final class CompositesBuilder */ private boolean hasMissingElements; + /** + * <code>true</code> if the composites contains some <code>unset</code> elements. + */ + private boolean containsUnset; + public CompositesBuilder(CType ctype) { this.ctype = ctype; @@ -85,7 +91,8 @@ public final class CompositesBuilder { if (value == null) containsNull = true; - + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + containsUnset = true; elementsList.get(i).add(value); } size++; @@ -128,6 +135,8 @@ public final class CompositesBuilder if (value == null) containsNull = true; + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + containsUnset = true; newComposite.add(values.get(j)); } @@ -177,6 +186,8 @@ public final class CompositesBuilder if (value.contains(null)) containsNull = true; + if (value.contains(ByteBufferUtil.UNSET_BYTE_BUFFER)) + containsUnset = true; newComposite.addAll(value); } @@ -236,6 +247,16 @@ public final class CompositesBuilder } /** + * Checks if the composites contains unset elements. + * + * @return <code>true</code> if the composites contains <code>unset</code> elements, <code>false</code> otherwise. + */ + public boolean containsUnset() + { + return containsUnset; + } + + /** * Builds the <code>Composites</code>. * * @return the composites http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/db/marshal/UserType.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index ce8bb43..ec97a7f 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -95,6 +95,11 @@ public class UserType extends TupleType return fieldNames.get(i); } + public String fieldNameAsString(int i) + { + return stringFieldNames.get(i); + } + public List<ByteBuffer> fieldNames() { return fieldNames; http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/transport/CBUtil.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/transport/CBUtil.java b/src/java/org/apache/cassandra/transport/CBUtil.java index 8f0dee0..73df554 100644 --- a/src/java/org/apache/cassandra/transport/CBUtil.java +++ b/src/java/org/apache/cassandra/transport/CBUtil.java @@ -374,6 +374,25 @@ public abstract class CBUtil return ByteBuffer.wrap(readRawBytes(slice)); } + public static ByteBuffer readBoundValue(ByteBuf cb, int protocolVersion) + { + int length = cb.readInt(); + if (length < 0) + { + if (protocolVersion < 4) // backward compatibility for pre-version 4 + return null; + if (length == -1) + return null; + else if (length == -2) + return ByteBufferUtil.UNSET_BYTE_BUFFER; + else + throw new ProtocolException("Invalid ByteBuf length " + length); + } + ByteBuf slice = cb.readSlice(length); + + return ByteBuffer.wrap(readRawBytes(slice)); + } + public static void writeValue(byte[] bytes, ByteBuf cb) { if (bytes == null) @@ -411,7 +430,7 @@ public abstract class CBUtil return 4 + (bytes == null ? 0 : bytes.remaining()); } - public static List<ByteBuffer> readValueList(ByteBuf cb) + public static List<ByteBuffer> readValueList(ByteBuf cb, int protocolVersion) { int size = cb.readUnsignedShort(); if (size == 0) @@ -419,7 +438,7 @@ public abstract class CBUtil List<ByteBuffer> l = new ArrayList<ByteBuffer>(size); for (int i = 0; i < size; i++) - l.add(readValue(cb)); + l.add(readBoundValue(cb, protocolVersion)); return l; } @@ -438,7 +457,7 @@ public abstract class CBUtil return size; } - public static Pair<List<String>, List<ByteBuffer>> readNameAndValueList(ByteBuf cb) + public static Pair<List<String>, List<ByteBuffer>> readNameAndValueList(ByteBuf cb, int protocolVersion) { int size = cb.readUnsignedShort(); if (size == 0) @@ -449,7 +468,7 @@ public abstract class CBUtil for (int i = 0; i < size; i++) { s.add(readString(cb)); - l.add(readValue(cb)); + l.add(readBoundValue(cb, protocolVersion)); } return Pair.create(s, l); } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/transport/messages/BatchMessage.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java index 88394e7..8f144d1 100644 --- a/src/java/org/apache/cassandra/transport/messages/BatchMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/BatchMessage.java @@ -61,7 +61,7 @@ public class BatchMessage extends Message.Request queryOrIds.add(MD5Digest.wrap(CBUtil.readBytes(body))); else throw new ProtocolException("Invalid query kind in BATCH messages. Must be 0 or 1 but got " + kind); - variables.add(CBUtil.readValueList(body)); + variables.add(CBUtil.readValueList(body, version)); } QueryOptions options = version < 3 ? QueryOptions.fromPreV3Batch(CBUtil.readConsistencyLevel(body)) http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java index 5f3e368..2b21376 100644 --- a/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java +++ b/src/java/org/apache/cassandra/transport/messages/ExecuteMessage.java @@ -47,7 +47,7 @@ public class ExecuteMessage extends Message.Request byte[] id = CBUtil.readBytes(body); if (version == 1) { - List<ByteBuffer> values = CBUtil.readValueList(body); + List<ByteBuffer> values = CBUtil.readValueList(body, version); ConsistencyLevel consistency = CBUtil.readConsistencyLevel(body); return new ExecuteMessage(MD5Digest.wrap(id), QueryOptions.fromProtocolV1(consistency, values)); } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/src/java/org/apache/cassandra/utils/ByteBufferUtil.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java index ce16d3c..4fea55e 100644 --- a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java +++ b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java @@ -78,6 +78,8 @@ import org.apache.cassandra.io.util.FileUtils; public class ByteBufferUtil { public static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.wrap(new byte[0]); + /** Represents an unset value in bound variables */ + public static final ByteBuffer UNSET_BYTE_BUFFER = ByteBuffer.wrap(new byte[]{}); @Inline public static int compareUnsigned(ByteBuffer o1, ByteBuffer o2) @@ -560,4 +562,5 @@ public class ByteBufferUtil int length = readShortLength(bb); return readBytes(bb, length); } + } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/CQLTester.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java index 6a7e52f..e8feb28 100644 --- a/test/unit/org/apache/cassandra/cql3/CQLTester.java +++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java @@ -59,6 +59,7 @@ import org.apache.cassandra.service.StorageService; import org.apache.cassandra.transport.Event; import org.apache.cassandra.transport.Server; import org.apache.cassandra.transport.messages.ResultMessage; +import org.apache.cassandra.utils.ByteBufferUtil; /** * Base class for CQL tests. @@ -319,6 +320,11 @@ public abstract class CQLTester return tables.get(tables.size() - 1); } + protected ByteBuffer unset() + { + return ByteBufferUtil.UNSET_BYTE_BUFFER; + } + protected void forcePreparedValues() { this.usePrepared = true; @@ -799,6 +805,11 @@ public abstract class CQLTester buffers[i] = null; continue; } + else if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + { + buffers[i] = ByteBufferUtil.UNSET_BYTE_BUFFER; + continue; + } try { http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/CollectionsTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/CollectionsTest.java b/test/unit/org/apache/cassandra/cql3/CollectionsTest.java index 3d266b7..0e1fbd6 100644 --- a/test/unit/org/apache/cassandra/cql3/CollectionsTest.java +++ b/test/unit/org/apache/cassandra/cql3/CollectionsTest.java @@ -209,4 +209,104 @@ public class CollectionsTest extends CQLTester assertRows(execute("SELECT l FROM %s WHERE k = 0"), row((Object) null)); } + + @Test + public void testMapWithUnsetValues() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, m map<text,text>)"); + // set up + Object m = map("k", "v"); + execute("INSERT INTO %s (k, m) VALUES (10, ?)", m); + assertRows(execute("SELECT m FROM %s WHERE k = 10"), + row(m) + ); + + // test putting an unset map, should not delete the contents + execute("INSERT INTO %s (k, m) VALUES (10, ?)", unset()); + assertRows(execute("SELECT m FROM %s WHERE k = 10"), + row(m) + ); + // test unset variables in a map update operaiotn, should not delete the contents + execute("UPDATE %s SET m['k'] = ? WHERE k = 10", unset()); + assertRows(execute("SELECT m FROM %s WHERE k = 10"), + row(m) + ); + assertInvalidMessage("Invalid unset map key", "UPDATE %s SET m[?] = 'foo' WHERE k = 10", unset()); + + // test unset value for map key + assertInvalidMessage("Invalid unset map key", "DELETE m[?] FROM %s WHERE k = 10", unset()); + } + + @Test + public void testListWithUnsetValues() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, l list<text>)"); + // set up + Object l = list("foo", "foo"); + execute("INSERT INTO %s (k, l) VALUES (10, ?)", l); + assertRows(execute("SELECT l FROM %s WHERE k = 10"), + row(l) + ); + + // replace list with unset value + execute("INSERT INTO %s (k, l) VALUES (10, ?)", unset()); + assertRows(execute("SELECT l FROM %s WHERE k = 10"), + row(l) + ); + + // add to position + execute("UPDATE %s SET l[1] = ? WHERE k = 10", unset()); + assertRows(execute("SELECT l FROM %s WHERE k = 10"), + row(l) + ); + + // set in index + assertInvalidMessage("Invalid unset value for list index", "UPDATE %s SET l[?] = 'foo' WHERE k = 10", unset()); + + // remove element by index + execute("DELETE l[?] FROM %s WHERE k = 10", unset()); + assertRows(execute("SELECT l FROM %s WHERE k = 10"), + row(l) + ); + + // remove all occurrences of element + execute("UPDATE %s SET l = l - ? WHERE k = 10", unset()); + assertRows(execute("SELECT l FROM %s WHERE k = 10"), + row(l) + ); + + // select with in clause + assertInvalidMessage("Invalid unset value for column k", "SELECT * FROM %s WHERE k IN ?", unset()); + assertInvalidMessage("Invalid unset value for column k", "SELECT * FROM %s WHERE k IN (?)", unset()); + } + + @Test + public void testSetWithUnsetValues() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, s set<text>)"); + + Object s = set("bar", "baz", "foo"); + execute("INSERT INTO %s (k, s) VALUES (10, ?)", s); + assertRows(execute("SELECT s FROM %s WHERE k = 10"), + row(s) + ); + + // replace set with unset value + execute("INSERT INTO %s (k, s) VALUES (10, ?)", unset()); + assertRows(execute("SELECT s FROM %s WHERE k = 10"), + row(s) + ); + + // add to set + execute("UPDATE %s SET s = s + ? WHERE k = 10", unset()); + assertRows(execute("SELECT s FROM %s WHERE k = 10"), + row(s) + ); + + // remove all occurrences of element + execute("UPDATE %s SET s = s - ? WHERE k = 10", unset()); + assertRows(execute("SELECT s FROM %s WHERE k = 10"), + row(s) + ); + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java b/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java index 3b1a826..c8b3a2f 100644 --- a/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java @@ -31,6 +31,7 @@ import org.junit.Test; import java.nio.ByteBuffer; import java.util.*; +import static org.apache.cassandra.utils.ByteBufferUtil.UNSET_BYTE_BUFFER; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; @@ -82,6 +83,7 @@ public class ColumnConditionTest assertTrue(isSatisfiedBy(bound, null, null)); assertFalse(isSatisfiedBy(bound, ONE, null)); assertFalse(isSatisfiedBy(bound, null, ONE)); + assertThrowsIRE(bound, UNSET_BYTE_BUFFER, ONE); // NEQ condition = ColumnCondition.condition(definition, new Constants.Value(ONE), Operator.NEQ); @@ -95,6 +97,7 @@ public class ColumnConditionTest assertFalse(isSatisfiedBy(bound, null, null)); assertTrue(isSatisfiedBy(bound, ONE, null)); assertTrue(isSatisfiedBy(bound, null, ONE)); + assertThrowsIRE(bound, UNSET_BYTE_BUFFER, ONE); // LT condition = ColumnCondition.condition(definition, new Constants.Value(ONE), Operator.LT); @@ -107,6 +110,7 @@ public class ColumnConditionTest assertFalse(isSatisfiedBy(bound, ByteBufferUtil.EMPTY_BYTE_BUFFER, ByteBufferUtil.EMPTY_BYTE_BUFFER)); assertThrowsIRE(bound, null, ONE); assertFalse(isSatisfiedBy(bound, ONE, null)); + assertThrowsIRE(bound, UNSET_BYTE_BUFFER, ONE); // LTE condition = ColumnCondition.condition(definition, new Constants.Value(ONE), Operator.LTE); @@ -119,6 +123,7 @@ public class ColumnConditionTest assertTrue(isSatisfiedBy(bound, ByteBufferUtil.EMPTY_BYTE_BUFFER, ByteBufferUtil.EMPTY_BYTE_BUFFER)); assertThrowsIRE(bound, null, ONE); assertFalse(isSatisfiedBy(bound, ONE, null)); + assertThrowsIRE(bound, UNSET_BYTE_BUFFER, ONE); // GT condition = ColumnCondition.condition(definition, new Constants.Value(ONE), Operator.GT); @@ -131,6 +136,7 @@ public class ColumnConditionTest assertFalse(isSatisfiedBy(bound, ByteBufferUtil.EMPTY_BYTE_BUFFER, ByteBufferUtil.EMPTY_BYTE_BUFFER)); assertThrowsIRE(bound, null, ONE); assertFalse(isSatisfiedBy(bound, ONE, null)); + assertThrowsIRE(bound, UNSET_BYTE_BUFFER, ONE); // GT condition = ColumnCondition.condition(definition, new Constants.Value(ONE), Operator.GTE); @@ -143,6 +149,7 @@ public class ColumnConditionTest assertTrue(isSatisfiedBy(bound, ByteBufferUtil.EMPTY_BYTE_BUFFER, ByteBufferUtil.EMPTY_BYTE_BUFFER)); assertThrowsIRE(bound, null, ONE); assertFalse(isSatisfiedBy(bound, ONE, null)); + assertThrowsIRE(bound, UNSET_BYTE_BUFFER, ONE); } private static List<ByteBuffer> list(ByteBuffer... values) http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/ContainsRelationTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/ContainsRelationTest.java b/test/unit/org/apache/cassandra/cql3/ContainsRelationTest.java index 60ea7e3..a987bb2 100644 --- a/test/unit/org/apache/cassandra/cql3/ContainsRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/ContainsRelationTest.java @@ -29,6 +29,9 @@ public class ContainsRelationTest extends CQLTester assertInvalidMessage("Unsupported null value for indexed column categories", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null); + assertInvalidMessage("Unsupported unset value for indexed column categories", + "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset()); + assertInvalidMessage("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING", "SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS ?", "xyz", "lmn", "notPresent"); assertEmpty(execute("SELECT * FROM %s WHERE account = ? AND categories CONTAINS ? AND categories CONTAINS ? ALLOW FILTERING", "xyz", "lmn", "notPresent")); @@ -59,6 +62,9 @@ public class ContainsRelationTest extends CQLTester assertInvalidMessage("Unsupported null value for indexed column categories", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null); + assertInvalidMessage("Unsupported unset value for indexed column categories", + "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset()); + assertInvalidMessage("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ?", "test", 5, "lmn", "notPresent"); @@ -108,6 +114,9 @@ public class ContainsRelationTest extends CQLTester assertInvalidMessage("Unsupported null value for indexed column categories", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, null); + assertInvalidMessage("Unsupported unset value for indexed column categories", + "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ?", "test", 5, unset()); + assertInvalidMessage("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS KEY ? AND categories CONTAINS KEY ?", "test", 5, "lmn", "notPresent"); @@ -144,6 +153,9 @@ public class ContainsRelationTest extends CQLTester assertInvalidMessage("Unsupported null value for indexed column categories", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, null); + assertInvalidMessage("Unsupported unset value for indexed column categories", + "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ?", "test", 5, unset()); + assertInvalidMessage("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING", "SELECT * FROM %s WHERE account = ? AND id = ? AND categories CONTAINS ? AND categories CONTAINS ?" , "test", 5, "foo", "notPresent"); http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/ModificationTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/ModificationTest.java b/test/unit/org/apache/cassandra/cql3/ModificationTest.java new file mode 100644 index 0000000..6397a15 --- /dev/null +++ b/test/unit/org/apache/cassandra/cql3/ModificationTest.java @@ -0,0 +1,112 @@ +/* + * 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.junit.Test; + +public class ModificationTest extends CQLTester +{ + @Test + public void testModificationWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, s text, i int)"); + + // insert using nulls + execute("INSERT INTO %s (k, s, i) VALUES (10, ?, ?)", "text", 10); + execute("INSERT INTO %s (k, s, i) VALUES (10, ?, ?)", null, null); + assertRows(execute("SELECT s, i FROM %s WHERE k = 10"), + row(null, null) // sending null deletes the data + ); + // insert using UNSET + execute("INSERT INTO %s (k, s, i) VALUES (11, ?, ?)", "text", 10); + execute("INSERT INTO %s (k, s, i) VALUES (11, ?, ?)", unset(), unset()); + assertRows(execute("SELECT s, i FROM %s WHERE k=11"), + row("text", 10) // unset columns does not delete the existing data + ); + + assertInvalidMessage("Invalid unset value for column k", "UPDATE %s SET i = 0 WHERE k = ?", unset()); + assertInvalidMessage("Invalid unset value for column k", "DELETE FROM %s WHERE k = ?", unset()); + assertInvalidMessage("Invalid unset value for argument in call to function blobasint", "SELECT * FROM %s WHERE k = blobAsInt(?)", unset()); + } + + @Test + public void testTtlWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, i int)"); + execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TTL ?", unset()); // treat as 'unlimited' + assertRows(execute("SELECT ttl(i) FROM %s"), + row(new Object[] {null}) + ); + } + + @Test + public void testTimestampWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, i int)"); + execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TIMESTAMP ?", unset()); // treat as 'now' + } + + @Test + public void testCounterUpdatesWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, c counter)"); + + // set up + execute("UPDATE %s SET c = c + 1 WHERE k = 10"); + assertRows(execute("SELECT c FROM %s WHERE k = 10"), + row(1L) + ); + // increment + execute("UPDATE %s SET c = c + ? WHERE k = 10", 1L); + assertRows(execute("SELECT c FROM %s WHERE k = 10"), + row(2L) + ); + execute("UPDATE %s SET c = c + ? WHERE k = 10", unset()); + assertRows(execute("SELECT c FROM %s WHERE k = 10"), + row(2L) // no change to the counter value + ); + // decrement + execute("UPDATE %s SET c = c - ? WHERE k = 10", 1L); + assertRows(execute("SELECT c FROM %s WHERE k = 10"), + row(1L) + ); + execute("UPDATE %s SET c = c - ? WHERE k = 10", unset()); + assertRows(execute("SELECT c FROM %s WHERE k = 10"), + row(1L) // no change to the counter value + ); + } + + @Test + public void testBatchWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, s text, i int)"); + + // test batch and update + String qualifiedTable = keyspace() + "." + currentTable(); + execute("BEGIN BATCH " + + "INSERT INTO %s (k, s, i) VALUES (100, 'batchtext', 7); " + + "INSERT INTO " + qualifiedTable + " (k, s, i) VALUES (111, 'batchtext', 7); " + + "UPDATE " + qualifiedTable + " SET s=?, i=? WHERE k = 100; " + + "UPDATE " + qualifiedTable + " SET s=?, i=? WHERE k=111; " + + "APPLY BATCH;", null, unset(), unset(), null); + assertRows(execute("SELECT k, s, i FROM %s where k in (100,111)"), + row(100, null, 7), + row(111, "batchtext", null) + ); + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java index 8bd4e97..b380b1e 100644 --- a/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java @@ -911,4 +911,26 @@ public class MultiColumnRelationTest extends CQLTester row(1, 1, 1)); } } + + @Test + public void testWithUnsetValues() throws Throwable + { + createTable("CREATE TABLE %s (k int, i int, j int, s text, PRIMARY KEY(k,i,j))"); + createIndex("CREATE INDEX s_index ON %s (s)"); + + assertInvalidMessage("Invalid unset value for tuple field number 0", + "SELECT * from %s WHERE (i, j) = (?,?) ALLOW FILTERING", unset(), 1); + assertInvalidMessage("Invalid unset value for tuple field number 0", + "SELECT * from %s WHERE (i, j) IN ((?,?)) ALLOW FILTERING", unset(), 1); + assertInvalidMessage("Invalid unset value for tuple field number 1", + "SELECT * from %s WHERE (i, j) > (1,?) ALLOW FILTERING", unset()); + assertInvalidMessage("Invalid unset value for tuple (i,j)", + "SELECT * from %s WHERE (i, j) = ? ALLOW FILTERING", unset()); + assertInvalidMessage("Invalid unset value for tuple (j)", + "SELECT * from %s WHERE i = ? AND (j) > ? ALLOW FILTERING", 1, unset()); + assertInvalidMessage("Invalid unset value for tuple (i,j)", + "SELECT * from %s WHERE (i, j) IN (?, ?) ALLOW FILTERING", unset(), tuple(1, 1)); + assertInvalidMessage("Invalid unset value for in(i,j)", + "SELECT * from %s WHERE (i, j) IN ? ALLOW FILTERING", unset()); + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/48f64468/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java index 4bbec81..2b8fbd4 100644 --- a/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java @@ -125,7 +125,7 @@ public class SingleColumnRelationTest extends CQLTester row("first", 2, 6, 2), row("first", 3, 7, 3)); - assertInvalidMessage("Invalid null value for IN restriction", + assertInvalidMessage("Invalid null value for column b", "select * from %s where a = ? and b in ? and c in ?", "first", null, Arrays.asList(7, 6)); assertRows(execute("select * from %s where a = ? and c >= ? and b in (?, ?)", "first", 6, 3, 2), @@ -505,4 +505,49 @@ public class SingleColumnRelationTest extends CQLTester assertRows(execute("SELECT * FROM %s WHERE a = ? AND c = ? AND d >= ? AND f = ? ALLOW FILTERING", 0, 1, 1, 5), row(0, 0, 1, 1, 1, 5)); } + + @Test + public void testFunctionCallWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, s text, i int)"); + + assertInvalidMessage("Invalid unset value for argument in call to function token", + "SELECT * FROM %s WHERE token(k) >= token(?)", unset()); + assertInvalidMessage("Invalid unset value for argument in call to function blobasint", + "SELECT * FROM %s WHERE k = blobAsInt(?)", unset()); + } + + @Test + public void testLimitWithUnset() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, i int)"); + execute("INSERT INTO %s (k, i) VALUES (1, 1)"); + execute("INSERT INTO %s (k, i) VALUES (2, 1)"); + assertRows(execute("SELECT k FROM %s LIMIT ?", unset()), // treat as 'unlimited' + row(1), + row(2) + ); + } + + @Test + public void testWithUnsetValues() throws Throwable + { + createTable("CREATE TABLE %s (k int, i int, j int, s text, PRIMARY KEY(k,i,j))"); + createIndex("CREATE INDEX s_index ON %s (s)"); + // partition key + assertInvalidMessage("Invalid unset value for column k", "SELECT * from %s WHERE k = ?", unset()); + assertInvalidMessage("Invalid unset value for column k", "SELECT * from %s WHERE k IN ?", unset()); + assertInvalidMessage("Invalid unset value for column k", "SELECT * from %s WHERE k IN(?)", unset()); + assertInvalidMessage("Invalid unset value for column k", "SELECT * from %s WHERE k IN(?,?)", 1, unset()); + // clustering column + assertInvalidMessage("Invalid unset value for column i", "SELECT * from %s WHERE k = 1 AND i = ?", unset()); + assertInvalidMessage("Invalid unset value for column i", "SELECT * from %s WHERE k = 1 AND i IN ?", unset()); + assertInvalidMessage("Invalid unset value for column i", "SELECT * from %s WHERE k = 1 AND i IN(?)", unset()); + assertInvalidMessage("Invalid unset value for column i", "SELECT * from %s WHERE k = 1 AND i IN(?,?)", 1, unset()); + assertInvalidMessage("Invalid unset value for column i", "SELECT * from %s WHERE i = ? ALLOW FILTERING", unset()); + // indexed column + assertInvalidMessage("Unsupported unset value for indexed column s", "SELECT * from %s WHERE s = ?", unset()); + // range + assertInvalidMessage("Invalid unset value for column i", "SELECT * from %s WHERE k = 1 AND i > ?", unset()); + } }