diff --git a/build.xml b/build.xml
index 7b67314f72..66a0d2e475 100644
--- a/build.xml
+++ b/build.xml
@@ -563,7 +563,7 @@
-
+
diff --git a/lib/commons-lang3-3.1.jar b/lib/commons-lang3-3.1.jar
deleted file mode 100644
index a85e539b17..0000000000
Binary files a/lib/commons-lang3-3.1.jar and /dev/null differ
diff --git a/lib/commons-lang3-3.11.jar b/lib/commons-lang3-3.11.jar
new file mode 100644
index 0000000000..bbaa8a61c8
Binary files /dev/null and b/lib/commons-lang3-3.11.jar differ
diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java
index ffdfc7c861..60ddaa9e5a 100644
--- a/src/java/org/apache/cassandra/db/ReadCommand.java
+++ b/src/java/org/apache/cassandra/db/ReadCommand.java
@@ -27,6 +27,7 @@ import java.util.function.Function;
import javax.annotation.Nullable;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@@ -60,6 +61,7 @@ import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.TableMetadataProvider;
import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.tracing.Tracing;
@@ -904,8 +906,22 @@ public abstract class ReadCommand extends AbstractReadQuery
}
}
- private static class Serializer implements IVersionedSerializer
+ @VisibleForTesting
+ public static class Serializer implements IVersionedSerializer
{
+ private final TableMetadataProvider schema;
+
+ public Serializer()
+ {
+ this(Schema.instance);
+ }
+
+ @VisibleForTesting
+ public Serializer(TableMetadataProvider schema)
+ {
+ this.schema = Objects.requireNonNull(schema, "schema");
+ }
+
private static int digestFlag(boolean isDigest)
{
return isDigest ? 0x01 : 0;
@@ -983,7 +999,7 @@ public abstract class ReadCommand extends AbstractReadQuery
boolean hasIndex = hasIndex(flags);
int digestVersion = isDigest ? (int)in.readUnsignedVInt() : 0;
- TableMetadata metadata = Schema.instance.getExistingTableMetadata(TableId.deserialize(in));
+ TableMetadata metadata = schema.getExistingTableMetadata(TableId.deserialize(in));
int nowInSec = in.readInt();
ColumnFilter columnFilter = ColumnFilter.serializer.deserialize(in, version, metadata);
RowFilter rowFilter = RowFilter.serializer.deserialize(in, version, metadata);
diff --git a/src/java/org/apache/cassandra/db/SchemaCQLHelper.java b/src/java/org/apache/cassandra/db/SchemaCQLHelper.java
index 6f9e5265a4..ded1692ba0 100644
--- a/src/java/org/apache/cassandra/db/SchemaCQLHelper.java
+++ b/src/java/org/apache/cassandra/db/SchemaCQLHelper.java
@@ -19,10 +19,13 @@
package org.apache.cassandra.db;
import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
+import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.schema.*;
@@ -32,6 +35,9 @@ import org.apache.cassandra.schema.*;
*/
public class SchemaCQLHelper
{
+ private static final Pattern EMPTY_TYPE_REGEX = Pattern.compile("empty", Pattern.LITERAL);
+ private static final String EMPTY_TYPE_QUOTED = Matcher.quoteReplacement("'org.apache.cassandra.db.marshal.EmptyType'");
+
/**
* Generates the DDL statement for a {@code schema.cql} snapshot file.
*/
@@ -155,4 +161,20 @@ public class SchemaCQLHelper
UTF8Type.instance.getString(name),
metadata)));
}
+
+ /**
+ * Converts the type to a CQL type. This method special cases empty and UDTs so the string can be used in a create
+ * statement.
+ *
+ * Special cases
+ *
+ *
empty - replaces with 'org.apache.cassandra.db.marshal.EmptyType'. empty is the tostring of the type in
+ * CQL but not allowed to create as empty, but fully qualified name is allowed
+ *
UserType - replaces with TupleType
+ *
+ */
+ public static String toCqlType(AbstractType> type)
+ {
+ return EMPTY_TYPE_REGEX.matcher(type.expandUserTypes().asCQL3Type().toString()).replaceAll(EMPTY_TYPE_QUOTED);
+ }
}
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
index 26146d7e4d..0a34785c7b 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
@@ -420,7 +420,7 @@ public abstract class AbstractType implements Comparator, Assignm
public long writtenLength(ByteBuffer value)
{
- assert value.hasRemaining();
+ assert value.hasRemaining() : "bytes should not be empty for type " + this;
return valueLengthIfFixed() >= 0
? value.remaining()
: TypeSizes.sizeofWithVIntLength(value);
diff --git a/src/java/org/apache/cassandra/db/marshal/EmptyType.java b/src/java/org/apache/cassandra/db/marshal/EmptyType.java
index 88d62c49ec..808402ff73 100644
--- a/src/java/org/apache/cassandra/db/marshal/EmptyType.java
+++ b/src/java/org/apache/cassandra/db/marshal/EmptyType.java
@@ -120,6 +120,14 @@ public class EmptyType extends AbstractType
return 0;
}
+ @Override
+ public long writtenLength(ByteBuffer value)
+ {
+ // default implemenation requires non-empty bytes but this always requires empty bytes, so special case
+ validate(value);
+ return 0;
+ }
+
@Override
public ByteBuffer readValue(DataInputPlus in)
{
diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java
index 3c023b7f5b..dfc726d9a1 100644
--- a/src/java/org/apache/cassandra/db/marshal/UserType.java
+++ b/src/java/org/apache/cassandra/db/marshal/UserType.java
@@ -24,6 +24,9 @@ import java.util.stream.Collectors;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
@@ -37,6 +40,7 @@ import org.apache.cassandra.utils.Pair;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.transform;
+import static org.apache.cassandra.cql3.ColumnIdentifier.maybeQuote;
/**
* A user defined type.
@@ -45,6 +49,10 @@ import static com.google.common.collect.Iterables.transform;
*/
public class UserType extends TupleType implements SchemaElement
{
+ private static final Logger logger = LoggerFactory.getLogger(UserType.class);
+
+ private static final ConflictBehavior CONFLICT_BEHAVIOR = ConflictBehavior.get();
+
public final String keyspace;
public final ByteBuffer name;
private final List fieldNames;
@@ -67,7 +75,9 @@ public class UserType extends TupleType implements SchemaElement
{
String stringFieldName = fieldNames.get(i).toString();
stringFieldNames.add(stringFieldName);
- fieldSerializers.put(stringFieldName, fieldTypes.get(i).getSerializer());
+ TypeSerializer> existing = fieldSerializers.put(stringFieldName, fieldTypes.get(i).getSerializer());
+ if (existing != null)
+ CONFLICT_BEHAVIOR.onConflict(keyspace, getNameAsString(), stringFieldName);
}
this.serializer = new UserTypeSerializer(fieldSerializers);
}
@@ -434,7 +444,7 @@ public class UserType extends TupleType implements SchemaElement
public String getCqlTypeName()
{
- return String.format("%s.%s", ColumnIdentifier.maybeQuote(keyspace), ColumnIdentifier.maybeQuote(getNameAsString()));
+ return String.format("%s.%s", maybeQuote(keyspace), maybeQuote(getNameAsString()));
}
@Override
@@ -490,4 +500,35 @@ public class UserType extends TupleType implements SchemaElement
return builder.toString();
}
+
+ private enum ConflictBehavior
+ {
+ LOG {
+ void onConflict(String keyspace, String name, String fieldName)
+ {
+ logger.error("Duplicate names found in UDT {}.{} for column {}",
+ maybeQuote(keyspace), maybeQuote(name), maybeQuote(fieldName));
+ }
+ },
+ REJECT {
+ @Override
+ void onConflict(String keyspace, String name, String fieldName)
+ {
+
+ throw new AssertionError(String.format("Duplicate names found in UDT %s.%s for column %s; " +
+ "to resolve set -D" + UDT_CONFLICT_BEHAVIOR + "=LOG on startup and remove the type",
+ maybeQuote(keyspace), maybeQuote(name), maybeQuote(fieldName)));
+ }
+ };
+
+ private static final String UDT_CONFLICT_BEHAVIOR = "cassandra.type.udt.conflict_behavior";
+
+ abstract void onConflict(String keyspace, String name, String fieldName);
+
+ static ConflictBehavior get()
+ {
+ String value = System.getProperty(UDT_CONFLICT_BEHAVIOR, REJECT.name());
+ return ConflictBehavior.valueOf(value);
+ }
+ }
}
diff --git a/src/java/org/apache/cassandra/net/Message.java b/src/java/org/apache/cassandra/net/Message.java
index 01ba5d4a43..e0a2e7f20b 100644
--- a/src/java/org/apache/cassandra/net/Message.java
+++ b/src/java/org/apache/cassandra/net/Message.java
@@ -683,7 +683,7 @@ public class Message
// int cast cuts off the high-order half of the timestamp, which we can assume remains
// the same between now and when the recipient reconstructs it.
out.writeInt((int) approxTime.translate().toMillisSinceEpoch(header.createdAtNanos));
- out.writeUnsignedVInt(1 + NANOSECONDS.toMillis(header.expiresAtNanos - header.createdAtNanos));
+ out.writeUnsignedVInt(NANOSECONDS.toMillis(header.expiresAtNanos - header.createdAtNanos));
out.writeUnsignedVInt(header.verb.id);
out.writeUnsignedVInt(header.flags);
serializeParams(header.params, out, version);
diff --git a/src/java/org/apache/cassandra/net/MessagingService.java b/src/java/org/apache/cassandra/net/MessagingService.java
index 0827f78019..c0f57f8092 100644
--- a/src/java/org/apache/cassandra/net/MessagingService.java
+++ b/src/java/org/apache/cassandra/net/MessagingService.java
@@ -19,9 +19,7 @@ package org.apache.cassandra.net;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -45,7 +43,6 @@ import org.apache.cassandra.utils.FBUtilities;
import static java.util.Collections.synchronizedList;
import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.concurrent.Stage.MUTATION;
import static org.apache.cassandra.utils.Throwables.maybeFail;
@@ -210,6 +207,20 @@ public final class MessagingService extends MessagingServiceMBeanImpl
static AcceptVersions accept_messaging = new AcceptVersions(minimum_version, current_version);
static AcceptVersions accept_streaming = new AcceptVersions(current_version, current_version);
+ public enum Version
+ {
+ VERSION_30(10),
+ VERSION_3014(11),
+ VERSION_40(12);
+
+ public final int value;
+
+ Version(int value)
+ {
+ this.value = value;
+ }
+ }
+
private static class MSHandle
{
public static final MessagingService instance = new MessagingService(false);
diff --git a/src/java/org/apache/cassandra/net/PingRequest.java b/src/java/org/apache/cassandra/net/PingRequest.java
index c02bd8099d..6b725479f0 100644
--- a/src/java/org/apache/cassandra/net/PingRequest.java
+++ b/src/java/org/apache/cassandra/net/PingRequest.java
@@ -19,6 +19,8 @@ package org.apache.cassandra.net;
import java.io.IOException;
+import com.google.common.annotations.VisibleForTesting;
+
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
@@ -30,7 +32,7 @@ import static org.apache.cassandra.net.ConnectionType.LARGE_MESSAGES;
/**
* Indicates to the recipient which {@link ConnectionType} should be used for the response.
*/
-class PingRequest
+public class PingRequest
{
static final PingRequest forUrgent = new PingRequest(URGENT_MESSAGES);
static final PingRequest forSmall = new PingRequest(SMALL_MESSAGES);
@@ -43,6 +45,18 @@ class PingRequest
this.connectionType = connectionType;
}
+ @VisibleForTesting
+ public static PingRequest get(ConnectionType type)
+ {
+ switch (type)
+ {
+ case URGENT_MESSAGES: return forUrgent;
+ case SMALL_MESSAGES: return forSmall;
+ case LARGE_MESSAGES: return forLarge;
+ default: throw new IllegalArgumentException("Unsupported type: " + type);
+ }
+ }
+
static IVersionedSerializer serializer = new IVersionedSerializer()
{
public void serialize(PingRequest t, DataOutputPlus out, int version) throws IOException
@@ -52,16 +66,7 @@ class PingRequest
public PingRequest deserialize(DataInputPlus in, int version) throws IOException
{
- ConnectionType type = ConnectionType.fromId(in.readByte());
-
- switch (type)
- {
- case URGENT_MESSAGES: return forUrgent;
- case SMALL_MESSAGES: return forSmall;
- case LARGE_MESSAGES: return forLarge;
- }
-
- throw new IllegalStateException();
+ return get(ConnectionType.fromId(in.readByte()));
}
public long serializedSize(PingRequest t, int version)
diff --git a/src/java/org/apache/cassandra/schema/Schema.java b/src/java/org/apache/cassandra/schema/Schema.java
index e2be6eed96..0498993d9d 100644
--- a/src/java/org/apache/cassandra/schema/Schema.java
+++ b/src/java/org/apache/cassandra/schema/Schema.java
@@ -51,7 +51,7 @@ import static java.lang.String.format;
import static com.google.common.collect.Iterables.size;
-public final class Schema
+public final class Schema implements TableMetadataProvider
{
public static final Schema instance = new Schema();
@@ -417,6 +417,7 @@ public final class Schema
: ksm.getTableOrViewNullable(table);
}
+ @Override
@Nullable
public TableMetadata getTableMetadata(TableId id)
{
@@ -445,22 +446,6 @@ public final class Schema
return getTableMetadata(descriptor.ksname, descriptor.cfname);
}
- /**
- * @throws UnknownTableException if the table couldn't be found in the metadata
- */
- public TableMetadata getExistingTableMetadata(TableId id) throws UnknownTableException
- {
- TableMetadata metadata = getTableMetadata(id);
- if (metadata != null)
- return metadata;
-
- String message =
- String.format("Couldn't find table with id %s. If a table was just created, this is likely due to the schema"
- + "not being fully propagated. Please wait for schema agreement on table creation.",
- id);
- throw new UnknownTableException(message, id);
- }
-
/* Function helpers */
/**
diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java
index 4c917dd951..7880c2af39 100644
--- a/src/java/org/apache/cassandra/schema/TableMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TableMetadata.java
@@ -911,7 +911,7 @@ public final class TableMetadata implements SchemaElement
return this;
}
- Builder addColumns(Iterable columns)
+ public Builder addColumns(Iterable columns)
{
columns.forEach(this::addColumn);
return this;
diff --git a/src/java/org/apache/cassandra/schema/TableMetadataProvider.java b/src/java/org/apache/cassandra/schema/TableMetadataProvider.java
new file mode 100644
index 0000000000..7c5ae8a2f0
--- /dev/null
+++ b/src/java/org/apache/cassandra/schema/TableMetadataProvider.java
@@ -0,0 +1,24 @@
+package org.apache.cassandra.schema;
+
+import javax.annotation.Nullable;
+
+import org.apache.cassandra.exceptions.UnknownTableException;
+
+public interface TableMetadataProvider
+{
+ @Nullable
+ TableMetadata getTableMetadata(TableId id);
+
+ default TableMetadata getExistingTableMetadata(TableId id) throws UnknownTableException
+ {
+ TableMetadata metadata = getTableMetadata(id);
+ if (metadata != null)
+ return metadata;
+
+ String message =
+ String.format("Couldn't find table with id %s. If a table was just created, this is likely due to the schema"
+ + "not being fully propagated. Please wait for schema agreement on table creation.",
+ id);
+ throw new UnknownTableException(message, id);
+ }
+}
diff --git a/src/java/org/apache/cassandra/serializers/TimestampSerializer.java b/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
index 49eb603ef5..ba35f6498c 100644
--- a/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/TimestampSerializer.java
@@ -101,14 +101,6 @@ public class TimestampSerializer implements TypeSerializer
private static final Pattern timestampPattern = Pattern.compile("^-?\\d+$");
- private static final FastThreadLocal FORMATTER = new FastThreadLocal()
- {
- protected SimpleDateFormat initialValue()
- {
- return new SimpleDateFormat("yyyy-MM-dd HH:mmXX");
- }
- };
-
private static final FastThreadLocal FORMATTER_UTC = new FastThreadLocal()
{
protected SimpleDateFormat initialValue()
@@ -188,7 +180,7 @@ public class TimestampSerializer implements TypeSerializer
public String toString(Date value)
{
- return value == null ? "" : FORMATTER.get().format(value);
+ return toStringUTC(value);
}
public String toStringUTC(Date value)
diff --git a/src/java/org/apache/cassandra/serializers/UserTypeSerializer.java b/src/java/org/apache/cassandra/serializers/UserTypeSerializer.java
index 472e39b42d..7af6c4a781 100644
--- a/src/java/org/apache/cassandra/serializers/UserTypeSerializer.java
+++ b/src/java/org/apache/cassandra/serializers/UserTypeSerializer.java
@@ -36,9 +36,10 @@ public class UserTypeSerializer extends BytesSerializer
public void validate(ByteBuffer bytes) throws MarshalException
{
ByteBuffer input = bytes.duplicate();
- int i = 0;
+ int i = -1; // first thing in the loop is to increment, so when starting this will get set to 0 and match the fields
for (Entry> entry : fields.entrySet())
{
+ i++;
// we allow the input to have less fields than declared so as to support field addition.
if (!input.hasRemaining())
return;
@@ -56,8 +57,14 @@ public class UserTypeSerializer extends BytesSerializer
throw new MarshalException(String.format("Not enough bytes to read %dth field %s", i, entry.getKey()));
ByteBuffer field = ByteBufferUtil.readBytes(input, size);
- entry.getValue().validate(field);
- i++;
+ try
+ {
+ entry.getValue().validate(field);
+ }
+ catch (MarshalException e)
+ {
+ throw new MarshalException(String.format("Failure validating the %dth field %s; %s", i, entry.getKey(), e.getMessage()), e);
+ }
}
// We're allowed to get less fields than declared, but not more
diff --git a/src/java/org/apache/cassandra/utils/ByteArrayUtil.java b/src/java/org/apache/cassandra/utils/ByteArrayUtil.java
new file mode 100644
index 0000000000..b97a1c5e46
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/ByteArrayUtil.java
@@ -0,0 +1,209 @@
+/*
+ * 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;
+
+public class ByteArrayUtil
+{
+ private ByteArrayUtil()
+ {
+
+ }
+
+ /*
+ * Methods for unpacking primitive values from byte arrays starting at
+ * given offsets.
+ */
+
+ public static boolean getBoolean(byte[] b)
+ {
+ return getBoolean(b, 0);
+ }
+
+ public static boolean getBoolean(byte[] b, int off)
+ {
+ return b[off] != 0;
+ }
+
+ /**
+ * @return signed short encoded as big endian
+ */
+ public static short getShort(byte[] b)
+ {
+ return getShort(b, 0);
+ }
+
+ /**
+ * @return signed short from the given offset encoded as big endian
+ */
+ public static short getShort(byte[] b, int off)
+ {
+ return (short) (b[off ] << 8 |
+ b[off + 1] & 255);
+ }
+
+ /**
+ * @return signed int encoded as big endian
+ */
+ public static int getInt(byte[] b)
+ {
+ return getInt(b, 0);
+ }
+
+ /**
+ * @return signed int from the given offset encoded as big endian
+ */
+ public static int getInt(byte[] b, int off)
+ {
+ return (b[off ] & 255) << 24 |
+ (b[off + 1] & 255) << 16 |
+ (b[off + 2] & 255) << 8 |
+ (b[off + 3] & 255);
+ }
+
+ /**
+ * @return signed float encoded as big endian
+ */
+ public static float getFloat(byte[] b)
+ {
+ return getFloat(b, 0);
+ }
+
+ /**
+ * @return signed float from the given offset encoded as big endian
+ */
+ public static float getFloat(byte[] b, int off)
+ {
+ return Float.intBitsToFloat(getInt(b, off));
+ }
+
+ /**
+ * @return signed long encoded as big endian
+ */
+ public static long getLong(byte[] b)
+ {
+ return getLong(b, 0);
+ }
+
+ /**
+ * @return signed long from the given offset encoded as big endian
+ */
+ public static long getLong(byte[] b, int off)
+ {
+ return ((long) b[off ] & 255L) << 56 |
+ ((long) b[off + 1] & 255L) << 48 |
+ ((long) b[off + 2] & 255L) << 40 |
+ ((long) b[off + 3] & 255L) << 32 |
+ ((long) b[off + 4] & 255L) << 24 |
+ ((long) b[off + 5] & 255L) << 16 |
+ ((long) b[off + 6] & 255L) << 8 |
+ ((long) b[off + 7] & 255L);
+ }
+
+ /**
+ * @return signed double from the given offset encoded as big endian
+ */
+ public static double getDouble(byte[] b)
+ {
+ return getDouble(b, 0);
+ }
+
+ /**
+ * @return signed double from the given offset encoded as big endian
+ */
+ public static double getDouble(byte[] b, int off)
+ {
+ return Double.longBitsToDouble(getLong(b, off));
+ }
+
+ /*
+ * Methods for packing primitive values into byte arrays starting at given
+ * offsets.
+ */
+
+ public static void putBoolean(byte[] b, int off, boolean val)
+ {
+ ensureCapacity(b, off, 1);
+ b[off] = (byte) (val ? 1 : 0);
+ }
+
+ /**
+ * Store a signed short at the given offset encoded as big endian
+ */
+ public static void putShort(byte[] b, int off, short val)
+ {
+ ensureCapacity(b, off, Short.BYTES);
+ b[off + 1] = (byte) (val );
+ b[off ] = (byte) (val >>> 8);
+ }
+
+ /**
+ * Store a signed int at the given offset encoded as big endian
+ */
+ public static void putInt(byte[] b, int off, int val)
+ {
+ ensureCapacity(b, off, Integer.BYTES);
+ b[off + 3] = (byte) (val );
+ b[off + 2] = (byte) (val >>> 8);
+ b[off + 1] = (byte) (val >>> 16);
+ b[off ] = (byte) (val >>> 24);
+ }
+
+ /**
+ * Store a signed float at the given offset encoded as big endian
+ */
+ public static void putFloat(byte[] b, int off, float val)
+ {
+ putInt(b, off, Float.floatToIntBits(val));
+ }
+
+ /**
+ * Store a signed long at the given offset encoded as big endian
+ */
+ public static void putLong(byte[] b, int off, long val)
+ {
+ ensureCapacity(b, off, Long.BYTES);
+ b[off + 7] = (byte) (val );
+ b[off + 6] = (byte) (val >>> 8);
+ b[off + 5] = (byte) (val >>> 16);
+ b[off + 4] = (byte) (val >>> 24);
+ b[off + 3] = (byte) (val >>> 32);
+ b[off + 2] = (byte) (val >>> 40);
+ b[off + 1] = (byte) (val >>> 48);
+ b[off ] = (byte) (val >>> 56);
+ }
+
+ /**
+ * Store a signed double at the given offset encoded as big endian
+ */
+ public static void putDouble(byte[] b, int off, double val)
+ {
+ putLong(b, off, Double.doubleToLongBits(val));
+ }
+
+ private static void ensureCapacity(byte[] b, int off, int len)
+ {
+ int writable = b.length - off;
+ if (writable < len)
+ {
+ if (writable < 0)
+ throw new IndexOutOfBoundsException("Attempted to write to offset " + off + " but array length is " + b.length);
+ throw new IndexOutOfBoundsException("Attempted to write " + len + " bytes to array with remaining capacity of " + writable);
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/utils/FailingConsumer.java b/src/java/org/apache/cassandra/utils/FailingConsumer.java
new file mode 100644
index 0000000000..93cec10871
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/FailingConsumer.java
@@ -0,0 +1,42 @@
+/*
+ * 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.function.Consumer;
+
+public interface FailingConsumer extends Consumer
+{
+ void doAccept(T t) throws Throwable;
+
+ default void accept(T t)
+ {
+ try
+ {
+ doAccept(t);
+ }
+ catch (Throwable e)
+ {
+ throw Throwables.throwAsUncheckedException(e);
+ }
+ }
+
+ static FailingConsumer orFail(FailingConsumer fn)
+ {
+ return fn;
+ }
+}
diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/TupleTypeTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/TupleTypeTest.java
index 28430cb03b..f9ef4cc253 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/entities/TupleTypeTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/entities/TupleTypeTest.java
@@ -17,13 +17,35 @@
*/
package org.apache.cassandra.cql3.validation.entities;
+import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
import org.junit.Test;
import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.db.SchemaCQLHelper;
+import org.apache.cassandra.db.marshal.TupleType;
+import org.apache.cassandra.utils.AbstractTypeGenerators.TypeSupport;
+import org.quicktheories.core.Gen;
+import org.quicktheories.generators.SourceDSL;
+
+import static org.apache.cassandra.db.SchemaCQLHelper.toCqlType;
+import static org.apache.cassandra.utils.AbstractTypeGenerators.getTypeSupport;
+import static org.apache.cassandra.utils.AbstractTypeGenerators.primitiveTypeGen;
+import static org.apache.cassandra.utils.AbstractTypeGenerators.tupleTypeGen;
+import static org.apache.cassandra.utils.FailingConsumer.orFail;
+import static org.apache.cassandra.utils.Generators.filter;
+import static org.quicktheories.QuickTheory.qt;
public class TupleTypeTest extends CQLTester
{
@@ -233,5 +255,140 @@ public class TupleTypeTest extends CQLTester
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mmX", Locale.ENGLISH);
assertRows(execute("SELECT tdemo FROM %s"), row(tuple( df.parse("2017-02-03 03:05+0000"), "Europe")));
}
+
+ @Test
+ public void tuplePartitionReadWrite()
+ {
+ qt().withExamples(100).withShrinkCycles(0).forAll(typesAndRowsGen()).checkAssert(orFail(testcase -> {
+ TupleType tupleType = testcase.type;
+ createTable("CREATE TABLE %s (id " + toCqlType(tupleType) + ", value int, PRIMARY KEY(id))");
+ SortedMap map = new TreeMap<>(Comparator.comparing(currentTableMetadata().partitioner::decorateKey));
+ int count = 0;
+ for (ByteBuffer value : testcase.uniqueRows)
+ {
+ map.put(value, count);
+ ByteBuffer[] tupleBuffers = tupleType.split(value);
+
+ // use cast to avoid warning
+ execute("INSERT INTO %s (id, value) VALUES (?, ?)", tuple((Object[]) tupleBuffers), count);
+
+ assertRows(execute("SELECT * FROM %s WHERE id = ?", tuple((Object[]) tupleBuffers)),
+ row(tuple((Object[]) tupleBuffers), count));
+ count++;
+ }
+ assertRows(execute("SELECT * FROM %s LIMIT 100"),
+ map.entrySet().stream().map(e -> row(e.getKey(), e.getValue())).toArray(Object[][]::new));
+ }));
+ }
+
+ @Test
+ public void tupleCkReadWriteAsc()
+ {
+ tupleCkReadWrite(Order.ASC);
+ }
+
+ @Test
+ public void tupleCkReadWriteDesc()
+ {
+ tupleCkReadWrite(Order.DESC);
+ }
+
+ private void tupleCkReadWrite(Order order)
+ {
+ // for some reason this test is much slower than the partition key test: with 100 examples partition key is 6s and these tests were 20-30s
+ qt().withExamples(50).withShrinkCycles(0).forAll(typesAndRowsGen()).checkAssert(orFail(testcase -> {
+ TupleType tupleType = testcase.type;
+ createTable("CREATE TABLE %s (pk int, ck " + toCqlType(tupleType) + ", value int, PRIMARY KEY(pk, ck))" +
+ " WITH CLUSTERING ORDER BY (ck "+order.name()+")");
+ String cql = SchemaCQLHelper.getTableMetadataAsCQL(currentTableMetadata(), false, false, false);
+ SortedMap map = new TreeMap<>(order.apply(tupleType));
+ int count = 0;
+ for (ByteBuffer value : testcase.uniqueRows)
+ {
+ map.put(value, count);
+ ByteBuffer[] tupleBuffers = tupleType.split(value);
+
+ // use cast to avoid warning
+ execute("INSERT INTO %s (pk, ck, value) VALUES (?, ?, ?)", 1, tuple((Object[]) tupleBuffers), count);
+
+ assertRows(execute("SELECT * FROM %s WHERE pk = ? AND ck = ?", 1, tuple((Object[]) tupleBuffers)),
+ row(1, tuple((Object[]) tupleBuffers), count));
+ count++;
+ }
+ UntypedResultSet results = execute("SELECT * FROM %s LIMIT 100");
+ assertRows(results,
+ map.entrySet().stream().map(e -> row(1, e.getKey(), e.getValue())).toArray(Object[][]::new));
+ }));
+ }
+
+ private static final class TypeAndRows
+ {
+ TupleType type;
+ List uniqueRows;
+ }
+
+ private static Gen typesAndRowsGen()
+ {
+ return typesAndRowsGen(10);
+ }
+
+ private static Gen typesAndRowsGen(int numRows)
+ {
+ Gen typeGen = tupleTypeGen(primitiveTypeGen(), SourceDSL.integers().between(1, 10));
+ Set distinctRows = new HashSet<>(numRows); // reuse the memory
+ Gen gen = rnd -> {
+ TypeAndRows c = new TypeAndRows();
+ c.type = typeGen.generate(rnd);
+ TypeSupport support = getTypeSupport(c.type);
+ Gen valueGen = filter(support.valueGen, b -> b.remaining() <= Short.MAX_VALUE);
+ valueGen = filter(valueGen, 20, v -> !distinctRows.contains(v));
+
+ distinctRows.clear();
+ for (int i = 0; i < numRows; i++)
+ {
+ try
+ {
+ assert distinctRows.add(valueGen.generate(rnd)) : "unable to add distinct row";
+ }
+ catch (IllegalStateException e)
+ {
+ // gave up trying to find values... so just try with how ever many rows we could
+ logger.warn("Unable to generate enough distinct rows; using {} rows", distinctRows.size());
+ break;
+ }
+ }
+ c.uniqueRows = new ArrayList<>(distinctRows);
+ return c;
+ };
+ gen = gen.describedAs(c -> c.type.asCQL3Type().toString());
+ return gen;
+ }
+
+ private enum Order {
+ ASC
+ {
+ Comparator apply(Comparator c)
+ {
+ return c;
+ }
+ },
+ DESC
+ {
+ Comparator apply(Comparator c)
+ {
+ return c.reversed();
+ }
+ };
+
+ abstract Comparator apply(Comparator c);
+ }
+
+ private static List