diff --git a/CHANGES.txt b/CHANGES.txt index 65becfbee9..c7da4908cc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,7 @@ 0.8.1 * add support for comparator parameters and a generic ReverseType (CASSANDRA-2355) + * add CompositeType and DynamicCompositeType (CASSANDRA-2231) 0.8.0-? * faster flushes and compaction from fixing excessively pessimistic diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java new file mode 100644 index 0000000000..5029b811e2 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/AbstractCompositeType.java @@ -0,0 +1,294 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cassandra.db.marshal; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.sql.Types; + +import org.apache.cassandra.utils.ByteBufferUtil; + +/** + * A class avoiding class duplication between CompositeType and + * DynamicCompositeType. + * Those two differs only in that for DynamicCompositeType, the comparators + * are in the encoded column name at the front of each component. + */ +public abstract class AbstractCompositeType extends AbstractType +{ + // changes bb position + protected static int getShortLength(ByteBuffer bb) + { + int length = (bb.get() & 0xFF) << 8; + return length | (bb.get() & 0xFF); + } + + // changes bb position + protected static void putShortLength(ByteBuffer bb, int length) + { + bb.put((byte) ((length >> 8) & 0xFF)); + bb.put((byte) (length & 0xFF)); + } + + // changes bb position + protected static ByteBuffer getBytes(ByteBuffer bb, int length) + { + ByteBuffer copy = bb.duplicate(); + copy.limit(copy.position() + length); + bb.position(bb.position() + length); + return copy; + } + + // changes bb position + protected static ByteBuffer getWithShortLength(ByteBuffer bb) + { + int length = getShortLength(bb); + return getBytes(bb, length); + } + + public int compare(ByteBuffer o1, ByteBuffer o2) + { + if (null == o1) + return null == o2 ? 0 : -1; + + ByteBuffer bb1 = o1.duplicate(); + ByteBuffer bb2 = o2.duplicate(); + int i = 0; + + while (bb1.remaining() > 0 && bb2.remaining() > 0) + { + AbstractType comparator = getNextComparator(i, bb1, bb2); + + ByteBuffer value1 = getWithShortLength(bb1); + ByteBuffer value2 = getWithShortLength(bb2); + + int cmp = comparator.compare(value1, value2); + if (cmp != 0) + return cmp; + + byte b1 = bb1.get(); + byte b2 = bb2.get(); + if (b1 < 0) + { + if (b2 >= 0) + return -1; + } + else if (b1 > 0) + { + if (b2 <= 0) + return 1; + } + else + { + // b1 == 0 + if (b2 != 0) + return -b2; + } + ++i; + } + + if (bb1.remaining() == 0) + return bb2.remaining() == 0 ? 0 : -1; + + // bb1.remaining() > 0 && bb2.remaining() == 0 + return 1; + } + + public String getString(ByteBuffer bytes) + { + StringBuilder sb = new StringBuilder(); + ByteBuffer bb = bytes.duplicate(); + int i = 0; + + while (bb.remaining() > 0) + { + if (bb.remaining() != bytes.remaining()) + sb.append(":"); + + AbstractType comparator = getAndAppendNextComparator(i, bb, sb); + ByteBuffer value = getWithShortLength(bb); + + sb.append(comparator.getString(value)); + + byte b = bb.get(); + if (b != 0) + { + sb.append(":!"); + break; + } + ++i; + } + return sb.toString(); + } + + /* + * FIXME: this would break if some of the component string representation + * contains ':'. None of our current comparator do so, so this is probably + * not an urgent matter, but this could break for custom comparator. + * (DynamicCompositeType would break on '@' too) + */ + public ByteBuffer fromString(String source) + { + String[] parts = source.split(":"); + List components = new ArrayList(); + List comparators = new ArrayList(); + int totalLength = 0, i = 0; + boolean lastByteIsOne = false; + + for (String part : parts) + { + if (part.equals("!")) + { + lastByteIsOne = true; + break; + } + + ParsedComparator p = parseNextComparator(i, part); + AbstractType type = p.getAbstractType(); + part = p.getRemainingPart(); + + ByteBuffer component = type.fromString(part); + totalLength += p.getComparatorSerializedSize() + 2 + component.remaining() + 1; + components.add(component); + comparators.add(p); + ++i; + } + + ByteBuffer bb = ByteBuffer.allocate(totalLength); + i = 0; + for (ByteBuffer component : components) + { + comparators.get(i).serializeComparator(bb); + putShortLength(bb, component.remaining()); + bb.put(component); // it's ok to consume component as we won't use it anymore + bb.put((byte)0); + ++i; + } + if (lastByteIsOne) + bb.put(bb.limit() - 1, (byte)1); + + bb.rewind(); + return bb; + } + + public void validate(ByteBuffer bytes) throws MarshalException + { + ByteBuffer bb = bytes.duplicate(); + + int i = 0; + while (bb.remaining() > 0) + { + AbstractType comparator = validateNextComparator(i, bb); + + if (bb.remaining() < 2) + throw new MarshalException("Not enough bytes to read value size of component " + i); + int length = getShortLength(bb); + + if (bb.remaining() < length) + throw new MarshalException("Not enough bytes to read value of component " + i); + ByteBuffer value = getBytes(bb, length); + + comparator.validate(value); + + if (bb.remaining() == 0) + throw new MarshalException("Not enough bytes to read the end-of-component byte of component" + i); + byte b = bb.get(); + if (b != 0 && bb.remaining() != 0) + throw new MarshalException("Invalid bytes remaining after an end-of-component at component" + i); + ++i; + } + } + + public ByteBuffer compose(ByteBuffer bytes) + { + return bytes; + } + + public ByteBuffer decompose(ByteBuffer value) + { + return value; + } + + public Class getType() + { + return ByteBuffer.class; + } + + public String toString(ByteBuffer value) + { + return getString(value); + } + + /* + * JDBC metadata. For now we don't allow the use of compositeType with + * JDBC. We'll have to figure out what is the best solution here. + */ + public boolean isSigned() + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + public boolean isCaseSensitive() + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + public boolean isCurrency() + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + public int getPrecision(ByteBuffer obj) + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + public int getScale(ByteBuffer obj) + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + public int getJdbcType() + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + public boolean needsQuotes() + { + throw new UnsupportedOperationException("Not support for JDBC yet"); + } + + abstract protected AbstractType getNextComparator(int i, ByteBuffer bb); + abstract protected AbstractType getNextComparator(int i, ByteBuffer bb1, ByteBuffer bb2); + abstract protected AbstractType getAndAppendNextComparator(int i, ByteBuffer bb, StringBuilder sb); + abstract protected ParsedComparator parseNextComparator(int i, String part); + abstract protected AbstractType validateNextComparator(int i, ByteBuffer bb) throws MarshalException; + + protected static interface ParsedComparator + { + AbstractType getAbstractType(); + String getRemainingPart(); + int getComparatorSerializedSize(); + void serializeComparator(ByteBuffer bb); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/CompositeType.java b/src/java/org/apache/cassandra/db/marshal/CompositeType.java new file mode 100644 index 0000000000..d1a4ca88fa --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/CompositeType.java @@ -0,0 +1,144 @@ +/** + * 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.db.marshal; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.utils.ByteBufferUtil; + +/* + * The encoding of a CompositeType column name should be: + * ... + * where is: + * <'end-of-component' byte> + * where is a 2 bytes unsigned short the and the + * 'end-of-component' byte should always be 0 for actual column name. + * However, it can set to 1 for query bounds. This allows to query for the + * equivalent of 'give me the full super-column'. That is, if during a slice + * query uses: + * start = <3><"foo".getBytes()><0> + * end = <3><"foo".getBytes()><1> + * then he will be sure to get *all* the columns whose first component is "foo". + * If for a component, the 'end-of-component' is != 0, there should not be any + * following component. The end-of-component can also be -1 to allow + * non-inclusive query. For instance: + * start = <3><"foo".getBytes()><-1> + * allows to query everything that is greater than <3><"foo".getBytes()>, but + * not <3><"foo".getBytes()> itself. + */ +public class CompositeType extends AbstractCompositeType +{ + // package protected for unit tests sake + final List types; + + // interning instances + private static final Map, CompositeType> instances = new HashMap, CompositeType>(); + + public static CompositeType getInstance(TypeParser parser) throws ConfigurationException + { + return getInstance(parser.getTypeParameters()); + } + + public static synchronized CompositeType getInstance(List types) throws ConfigurationException + { + if (types == null || types.isEmpty()) + throw new ConfigurationException("Nonsensical empty parameter list for CompositeType"); + + CompositeType ct = instances.get(types); + if (ct == null) + { + ct = new CompositeType(types); + instances.put(types, ct); + } + return ct; + } + + private CompositeType(List types) + { + this.types = types; + } + + protected AbstractType getNextComparator(int i, ByteBuffer bb) + { + return types.get(i); + } + + protected AbstractType getNextComparator(int i, ByteBuffer bb1, ByteBuffer bb2) + { + return types.get(i); + } + + protected AbstractType getAndAppendNextComparator(int i, ByteBuffer bb, StringBuilder sb) + { + return types.get(i); + } + + protected ParsedComparator parseNextComparator(int i, String part) + { + return new StaticParsedComparator(types.get(i), part); + } + + protected AbstractType validateNextComparator(int i, ByteBuffer bb) throws MarshalException + { + if (i >= types.size()) + throw new MarshalException("Too many bytes for comparator"); + return types.get(i); + } + + private static class StaticParsedComparator implements ParsedComparator + { + final AbstractType type; + final String part; + + StaticParsedComparator(AbstractType type, String part) + { + this.type = type; + this.part = part; + } + + public AbstractType getAbstractType() + { + return type; + } + + public String getRemainingPart() + { + return part; + } + + public int getComparatorSerializedSize() + { + return 0; + } + + public void serializeComparator(ByteBuffer bb) {} + } + + @Override + public String toString() + { + return getClass().getName() + TypeParser.stringifyTypeParameters(types); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java b/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java new file mode 100644 index 0000000000..592f3923e4 --- /dev/null +++ b/src/java/org/apache/cassandra/db/marshal/DynamicCompositeType.java @@ -0,0 +1,266 @@ +/** + * 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.db.marshal; + +import java.nio.charset.CharacterCodingException; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* + * The encoding of a DynamicCompositeType column name should be: + * ... + * where is: + * <'end-of-component' byte> + * where: + * - : either the comparator full name, or a declared + * aliases. This is at least 2 bytes (those 2 bytes are called header in + * the following). If the first bit of the header is 1, then this + * comparator part is an alias, otherwise it's a comparator full name: + * - aliases: the actual alias is the 2nd byte of header taken as a + * character. The whole is thus 2 byte long. + * - comparator full name: the header is the length of the remaining + * part. The remaining part is the UTF-8 encoded comparator class + * name. + * - : the component value bytes preceded by 2 byte containing the + * size of value (see CompositeType). + * - 'end-of-component' byte is defined as in CompositeType + */ +public class DynamicCompositeType extends AbstractCompositeType +{ + private static final Logger logger = LoggerFactory.getLogger(DynamicCompositeType.class); + + private final Map aliases; + + // interning instances + private static final Map, DynamicCompositeType> instances = new HashMap, DynamicCompositeType>(); + + public static synchronized DynamicCompositeType getInstance(TypeParser parser) throws ConfigurationException + { + return getInstance(parser.getAliasParameters()); + } + + public static synchronized DynamicCompositeType getInstance(Map aliases) + { + DynamicCompositeType dct = instances.get(aliases); + if (dct == null) + { + dct = new DynamicCompositeType(aliases); + instances.put(aliases, dct); + } + return dct; + } + + private DynamicCompositeType(Map aliases) + { + this.aliases = aliases; + } + + private AbstractType getComparator(ByteBuffer bb) + { + try + { + int header = getShortLength(bb); + if ((header & 0x8000) == 0) + { + String name = ByteBufferUtil.string(getBytes(bb, header)); + return TypeParser.parse(name); + } + else + { + return aliases.get((byte)(header & 0xFF)); + } + } + catch (CharacterCodingException e) + { + throw new RuntimeException(e); + } + catch (ConfigurationException e) + { + throw new RuntimeException(e); + } + } + + protected AbstractType getNextComparator(int i, ByteBuffer bb) + { + return getComparator(bb); + } + + protected AbstractType getNextComparator(int i, ByteBuffer bb1, ByteBuffer bb2) + { + AbstractType comp1 = getComparator(bb1); + AbstractType comp2 = getComparator(bb2); + + // This rely on comparator always being singleton instances + if (comp1 != comp2) + { + logger.error("Mismatch between {} and {}", comp1, comp2); + throw new RuntimeException("Comparator mismatch while comparing two DynamicCompositeType colum name"); + } + return comp1; + } + + protected AbstractType getAndAppendNextComparator(int i, ByteBuffer bb, StringBuilder sb) + { + try + { + int header = getShortLength(bb); + if ((header & 0x8000) == 0) + { + String name = ByteBufferUtil.string(getBytes(bb, header)); + sb.append(name).append("@"); + return TypeParser.parse(name); + } + else + { + sb.append((char)(header & 0xFF)).append("@"); + return aliases.get((byte)(header & 0xFF)); + } + } + catch (CharacterCodingException e) + { + throw new RuntimeException(e); + } + catch (ConfigurationException e) + { + throw new RuntimeException(e); + } + } + + protected ParsedComparator parseNextComparator(int i, String part) + { + return new DynamicParsedComparator(part); + } + + protected AbstractType validateNextComparator(int i, ByteBuffer bb) throws MarshalException + { + AbstractType comparator = null; + if (bb.remaining() < 2) + throw new MarshalException("Not enough bytes to header of the comparator part of component " + i); + int header = getShortLength(bb); + if ((header & 0x8000) == 0) + { + if (bb.remaining() < header) + throw new MarshalException("Not enough bytes to read comparator name of component " + i); + + ByteBuffer value = getBytes(bb, header); + try + { + comparator = TypeParser.parse(ByteBufferUtil.string(value)); + } + catch (Exception e) + { + // we'll deal with this below since comparator == null + } + } + else + { + comparator = aliases.get((byte)(header & 0xFF)); + } + + if (comparator == null) + throw new MarshalException("Cannot find comparator for component " + i); + else + return comparator; + } + + private class DynamicParsedComparator implements ParsedComparator + { + final AbstractType type; + final boolean isAlias; + final String comparatorName; + final String remainingPart; + + DynamicParsedComparator(String part) + { + String[] splits = part.split("@"); + if (splits.length != 2) + throw new IllegalArgumentException("Invalid component representation: " + part); + + comparatorName = splits[0]; + remainingPart = splits[1]; + + try + { + AbstractType t = null; + if (comparatorName.length() == 1) + { + // try for an alias + // Note: the char to byte cast is theorically bogus for unicode character. I take full + // responsibility if someone get hit by this (without making it on purpose) + t = aliases.get((byte)comparatorName.charAt(0)); + } + isAlias = t != null; + if (!isAlias) + { + t = TypeParser.parse(comparatorName); + } + type = t; + } + catch (ConfigurationException e) + { + throw new IllegalArgumentException(e); + } + } + + public AbstractType getAbstractType() + { + return type; + } + + public String getRemainingPart() + { + return remainingPart; + } + + public int getComparatorSerializedSize() + { + return isAlias ? 2 : 2 + ByteBufferUtil.bytes(comparatorName).remaining(); + } + + public void serializeComparator(ByteBuffer bb) + { + int header = 0; + if (isAlias) + header = 0x8000 | ((byte)comparatorName.charAt(0)); + else + header = comparatorName.length(); + putShortLength(bb, header); + + if (!isAlias) + bb.put(ByteBufferUtil.bytes(comparatorName)); + } + } + + @Override + public String toString() + { + return getClass().getName() + TypeParser.stringifyAliasesParameters(aliases); + } +} diff --git a/src/java/org/apache/cassandra/db/marshal/TypeParser.java b/src/java/org/apache/cassandra/db/marshal/TypeParser.java index 2efe30af5a..4aef620dcd 100644 --- a/src/java/org/apache/cassandra/db/marshal/TypeParser.java +++ b/src/java/org/apache/cassandra/db/marshal/TypeParser.java @@ -27,6 +27,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.apache.commons.lang.StringUtils; + import org.apache.cassandra.config.ConfigurationException; import org.apache.cassandra.utils.FBUtilities; @@ -414,17 +416,7 @@ public class TypeParser public static String stringifyTypeParameters(List types) { StringBuilder sb = new StringBuilder(); - sb.append('('); - Iterator iter = types.iterator(); - if (iter.hasNext()) - { - sb.append(iter.next()); - } - while (iter.hasNext()) - { - sb.append(',').append(iter.next()); - } - sb.append(')'); + sb.append('(').append(StringUtils.join(types, ",")).append(')'); return sb.toString(); } } diff --git a/test/unit/org/apache/cassandra/SchemaLoader.java b/test/unit/org/apache/cassandra/SchemaLoader.java index a77d03e196..d4f3fdcc42 100644 --- a/test/unit/org/apache/cassandra/SchemaLoader.java +++ b/test/unit/org/apache/cassandra/SchemaLoader.java @@ -54,7 +54,7 @@ public class SchemaLoader } } - public static Collection schemaDefinition() + public static Collection schemaDefinition() throws ConfigurationException { List schema = new ArrayList(); @@ -78,6 +78,12 @@ public class SchemaLoader ColumnFamilyType st = ColumnFamilyType.Standard; ColumnFamilyType su = ColumnFamilyType.Super; AbstractType bytes = BytesType.instance; + + AbstractType composite = CompositeType.getInstance(Arrays.asList(new AbstractType[]{BytesType.instance, TimeUUIDType.instance, IntegerType.instance})); + Map aliases = new HashMap(); + aliases.put((byte)'b', BytesType.instance); + aliases.put((byte)'t', TimeUUIDType.instance); + AbstractType dynamicComposite = DynamicCompositeType.getInstance(aliases); // these column definitions will will be applied to the jdbc utf and integer column familes respectively. Map integerColumn = new HashMap(); @@ -134,7 +140,17 @@ public class SchemaLoader jdbcCFMD(ks1, "JdbcUtf8", UTF8Type.instance).columnMetadata(utf8Column), jdbcCFMD(ks1, "JdbcLong", LongType.instance), jdbcCFMD(ks1, "JdbcBytes", bytes), - jdbcCFMD(ks1, "JdbcAscii", AsciiType.instance))); + jdbcCFMD(ks1, "JdbcAscii", AsciiType.instance), + new CFMetaData(ks1, + "StandardComposite", + st, + composite, + null), + new CFMetaData(ks1, + "StandardDynamicComposite", + st, + dynamicComposite, + null))); // Keyspace 2 schema.add(new KSMetaData(ks2, diff --git a/test/unit/org/apache/cassandra/db/marshal/CompositeTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/CompositeTypeTest.java new file mode 100644 index 0000000000..54b240fc1f --- /dev/null +++ b/test/unit/org/apache/cassandra/db/marshal/CompositeTypeTest.java @@ -0,0 +1,268 @@ +/* +* 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.db.marshal; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +import org.junit.Test; +import static org.junit.Assert.fail; + +import org.apache.cassandra.CleanupHelper; +import org.apache.cassandra.Util; +import org.apache.cassandra.config.ConfigurationException; +import org.apache.cassandra.db.*; +import org.apache.cassandra.db.columniterator.IdentityQueryFilter; +import org.apache.cassandra.db.filter.QueryFilter; +import org.apache.cassandra.db.filter.QueryPath; +import org.apache.cassandra.utils.*; + +public class CompositeTypeTest extends CleanupHelper +{ + private static final String cfName = "StandardComposite"; + private static final CompositeType comparator; + static + { + List subComparators = new ArrayList(); + subComparators.add(BytesType.instance); + subComparators.add(TimeUUIDType.instance); + subComparators.add(IntegerType.instance); + try + { + comparator = CompositeType.getInstance(subComparators); + } + catch (ConfigurationException e) + { + throw new RuntimeException(e); + } + + } + + private static final int UUID_COUNT = 3; + private static final UUID[] uuids = new UUID[UUID_COUNT]; + static + { + for (int i = 0; i < UUID_COUNT; ++i) + uuids[i] = UUIDGen.makeType1UUIDFromHost(FBUtilities.getLocalAddress()); + } + + @Test + public void testEndOfComponent() + { + ByteBuffer[] cnames = { + createCompositeKey("test1", uuids[0], -1, false), + createCompositeKey("test1", uuids[1], 24, false), + createCompositeKey("test1", uuids[1], 42, false), + createCompositeKey("test1", uuids[1], 83, false), + createCompositeKey("test1", uuids[2], -1, false), + createCompositeKey("test1", uuids[2], 42, false), + }; + + ByteBuffer start = createCompositeKey("test1", uuids[1], -1, false); + ByteBuffer stop = createCompositeKey("test1", uuids[1], -1, true); + + for (int i = 0; i < 1; ++i) + { + assert comparator.compare(start, cnames[i]) > 0; + assert comparator.compare(stop, cnames[i]) > 0; + } + for (int i = 1; i < 4; ++i) + { + assert comparator.compare(start, cnames[i]) < 0; + assert comparator.compare(stop, cnames[i]) > 0; + } + for (int i = 4; i < cnames.length; ++i) + { + assert comparator.compare(start, cnames[i]) < 0; + assert comparator.compare(stop, cnames[i]) < 0; + } + } + + @Test + public void testGetString() + { + String test1Hex = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes("test1")); + ByteBuffer key = createCompositeKey("test1", uuids[1], 42, false); + assert comparator.getString(key).equals(test1Hex + ":" + uuids[1] + ":42"); + + key = createCompositeKey("test1", uuids[1], -1, true); + assert comparator.getString(key).equals(test1Hex + ":" + uuids[1] + ":!"); + } + + @Test + public void testFromString() + { + String test1Hex = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes("test1")); + ByteBuffer key = createCompositeKey("test1", uuids[1], 42, false); + assert key.equals(comparator.fromString(test1Hex + ":" + uuids[1] + ":42")); + + key = createCompositeKey("test1", uuids[1], -1, true); + assert key.equals(comparator.fromString(test1Hex + ":" + uuids[1] + ":!")); + } + + @Test + public void testValidate() + { + ByteBuffer key = createCompositeKey("test1", uuids[1], 42, false); + comparator.validate(key); + + key = createCompositeKey("test1", null, -1, false); + comparator.validate(key); + + key = createCompositeKey("test1", uuids[2], -1, true); + comparator.validate(key); + + key.get(); // make sure we're not aligned anymore + try + { + comparator.validate(key); + fail("Should not validate"); + } + catch (MarshalException e) {} + + key = ByteBuffer.allocate(3 + "test1".length() + 3 + 14); + key.putShort((short) "test1".length()); + key.put(ByteBufferUtil.bytes("test1")); + key.put((byte) 0); + key.putShort((short) 14); + key.rewind(); + try + { + comparator.validate(key); + fail("Should not validate"); + } + catch (MarshalException e) + { + assert e.toString().contains("TimeUUID should be 16 or 0 bytes"); + } + + key = createCompositeKey("test1", UUID.randomUUID(), 42, false); + try + { + comparator.validate(key); + fail("Should not validate"); + } + catch (MarshalException e) + { + assert e.toString().contains("Invalid version for TimeUUID type"); + } + } + + @Test + public void testFullRound() throws Exception + { + Table table = Table.open("Keyspace1"); + ColumnFamilyStore cfs = table.getColumnFamilyStore(cfName); + + ByteBuffer cname1 = createCompositeKey("test1", null, -1, false); + ByteBuffer cname2 = createCompositeKey("test1", uuids[0], 24, false); + ByteBuffer cname3 = createCompositeKey("test1", uuids[0], 42, false); + ByteBuffer cname4 = createCompositeKey("test2", uuids[0], -1, false); + ByteBuffer cname5 = createCompositeKey("test2", uuids[1], 42, false); + + ByteBuffer key = ByteBufferUtil.bytes("k"); + RowMutation rm = new RowMutation("Keyspace1", key); + addColumn(rm, cname5); + addColumn(rm, cname1); + addColumn(rm, cname4); + addColumn(rm, cname2); + addColumn(rm, cname3); + rm.apply(); + + ColumnFamily cf = cfs.getColumnFamily(QueryFilter.getIdentityFilter(Util.dk("k"), new QueryPath(cfName, null, null))); + + Iterator iter = cf.getSortedColumns().iterator(); + + assert iter.next().name().equals(cname1); + assert iter.next().name().equals(cname2); + assert iter.next().name().equals(cname3); + assert iter.next().name().equals(cname4); + assert iter.next().name().equals(cname5); + } + + @Test + public void testEmptyParametersNotallowed() + { + try + { + TypeParser.parse("CompositeType"); + fail("Shouldn't work"); + } + catch (ConfigurationException e) {} + + try + { + TypeParser.parse("CompositeType()"); + fail("Shouldn't work"); + } + catch (ConfigurationException e) {} + } + + + private void addColumn(RowMutation rm, ByteBuffer cname) + { + rm.add(new QueryPath(cfName, null , cname), ByteBufferUtil.EMPTY_BYTE_BUFFER, 0); + } + + private ByteBuffer createCompositeKey(String s, UUID uuid, int i, boolean lastIsOne) + { + ByteBuffer bytes = ByteBufferUtil.bytes(s); + int totalSize = 0; + if (s != null) + { + totalSize += 2 + bytes.remaining() + 1; + if (uuid != null) + { + totalSize += 2 + 16 + 1; + if (i != -1) + { + totalSize += 2 + 1 + 1; + } + } + } + + ByteBuffer bb = ByteBuffer.allocate(totalSize); + + if (s != null) + { + bb.putShort((short) bytes.remaining()); + bb.put(bytes); + bb.put(uuid == null && lastIsOne ? (byte)1 : (byte)0); + if (uuid != null) + { + bb.putShort((short) 16); + bb.put(UUIDGen.decompose(uuid)); + bb.put(i == -1 && lastIsOne ? (byte)1 : (byte)0); + if (i != -1) + { + // We are putting a byte only because our test use ints that fit in a byte *and* IntegerType.fromString() will + // return something compatible (i.e, putting a full int here would break 'fromStringTest') + bb.putShort((short) 1); + bb.put((byte)i); + bb.put(lastIsOne ? (byte)1 : (byte)0); + } + } + } + bb.rewind(); + return bb; + } +} diff --git a/test/unit/org/apache/cassandra/db/marshal/DynamicCompositeTypeTest.java b/test/unit/org/apache/cassandra/db/marshal/DynamicCompositeTypeTest.java new file mode 100644 index 0000000000..2d90531c96 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/marshal/DynamicCompositeTypeTest.java @@ -0,0 +1,246 @@ +/* +* 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.db.marshal; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.Test; +import static org.junit.Assert.fail; + +import org.apache.cassandra.CleanupHelper; +import org.apache.cassandra.Util; +import org.apache.cassandra.db.*; +import org.apache.cassandra.db.columniterator.IdentityQueryFilter; +import org.apache.cassandra.db.filter.QueryFilter; +import org.apache.cassandra.db.filter.QueryPath; +import org.apache.cassandra.utils.*; + +public class DynamicCompositeTypeTest extends CleanupHelper +{ + private static final String cfName = "StandardDynamicComposite"; + + private static final DynamicCompositeType comparator; + static + { + Map aliases = new HashMap(); + aliases.put((byte)'b', BytesType.instance); + aliases.put((byte)'t', TimeUUIDType.instance); + comparator = DynamicCompositeType.getInstance(aliases); + } + + private static final int UUID_COUNT = 3; + private static final UUID[] uuids = new UUID[UUID_COUNT]; + static + { + for (int i = 0; i < UUID_COUNT; ++i) + uuids[i] = UUIDGen.makeType1UUIDFromHost(FBUtilities.getLocalAddress()); + } + + @Test + public void testEndOfComponent() + { + ByteBuffer[] cnames = { + createDynamicCompositeKey("test1", uuids[0], -1, false), + createDynamicCompositeKey("test1", uuids[1], 24, false), + createDynamicCompositeKey("test1", uuids[1], 42, false), + createDynamicCompositeKey("test1", uuids[1], 83, false), + createDynamicCompositeKey("test1", uuids[2], -1, false), + createDynamicCompositeKey("test1", uuids[2], 42, false), + }; + + ByteBuffer start = createDynamicCompositeKey("test1", uuids[1], -1, false); + ByteBuffer stop = createDynamicCompositeKey("test1", uuids[1], -1, true); + + for (int i = 0; i < 1; ++i) + { + assert comparator.compare(start, cnames[i]) > 0; + assert comparator.compare(stop, cnames[i]) > 0; + } + for (int i = 1; i < 4; ++i) + { + assert comparator.compare(start, cnames[i]) < 0; + assert comparator.compare(stop, cnames[i]) > 0; + } + for (int i = 4; i < cnames.length; ++i) + { + assert comparator.compare(start, cnames[i]) < 0; + assert comparator.compare(stop, cnames[i]) < 0; + } + } + + @Test + public void testGetString() + { + String test1Hex = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes("test1")); + ByteBuffer key = createDynamicCompositeKey("test1", uuids[1], 42, false); + assert comparator.getString(key).equals("b@" + test1Hex + ":t@" + uuids[1] + ":IntegerType@42"); + + key = createDynamicCompositeKey("test1", uuids[1], -1, true); + assert comparator.getString(key).equals("b@" + test1Hex + ":t@" + uuids[1] + ":!"); + } + + @Test + public void testFromString() + { + String test1Hex = ByteBufferUtil.bytesToHex(ByteBufferUtil.bytes("test1")); + ByteBuffer key = createDynamicCompositeKey("test1", uuids[1], 42, false); + assert key.equals(comparator.fromString("b@" + test1Hex + ":t@" + uuids[1] + ":IntegerType@42")); + + key = createDynamicCompositeKey("test1", uuids[1], -1, true); + assert key.equals(comparator.fromString("b@" + test1Hex + ":t@" + uuids[1] + ":!")); + } + + @Test + public void testValidate() + { + ByteBuffer key = createDynamicCompositeKey("test1", uuids[1], 42, false); + comparator.validate(key); + + key = createDynamicCompositeKey("test1", null, -1, false); + comparator.validate(key); + + key = createDynamicCompositeKey("test1", uuids[2], -1, true); + comparator.validate(key); + + key.get(); // make sure we're not aligned anymore + try + { + comparator.validate(key); + fail("Should not validate"); + } + catch (MarshalException e) {} + + key = ByteBuffer.allocate(5 + "test1".length() + 5 + 14); + key.putShort((short) (0x8000 | 'b')); + key.putShort((short) "test1".length()); + key.put(ByteBufferUtil.bytes("test1")); + key.put((byte) 0); + key.putShort((short) (0x8000 | 't')); + key.putShort((short) 14); + key.rewind(); + try + { + comparator.validate(key); + fail("Should not validate"); + } + catch (MarshalException e) + { + assert e.toString().contains("TimeUUID should be 16 or 0 bytes"); + } + + key = createDynamicCompositeKey("test1", UUID.randomUUID(), 42, false); + try + { + comparator.validate(key); + fail("Should not validate"); + } + catch (MarshalException e) + { + assert e.toString().contains("Invalid version for TimeUUID type"); + } + } + + @Test + public void testFullRound() throws Exception + { + Table table = Table.open("Keyspace1"); + ColumnFamilyStore cfs = table.getColumnFamilyStore(cfName); + + ByteBuffer cname1 = createDynamicCompositeKey("test1", null, -1, false); + ByteBuffer cname2 = createDynamicCompositeKey("test1", uuids[0], 24, false); + ByteBuffer cname3 = createDynamicCompositeKey("test1", uuids[0], 42, false); + ByteBuffer cname4 = createDynamicCompositeKey("test2", uuids[0], -1, false); + ByteBuffer cname5 = createDynamicCompositeKey("test2", uuids[1], 42, false); + + ByteBuffer key = ByteBufferUtil.bytes("k"); + RowMutation rm = new RowMutation("Keyspace1", key); + addColumn(rm, cname5); + addColumn(rm, cname1); + addColumn(rm, cname4); + addColumn(rm, cname2); + addColumn(rm, cname3); + rm.apply(); + + ColumnFamily cf = cfs.getColumnFamily(QueryFilter.getIdentityFilter(Util.dk("k"), new QueryPath(cfName, null, null))); + + Iterator iter = cf.getSortedColumns().iterator(); + + assert iter.next().name().equals(cname1); + assert iter.next().name().equals(cname2); + assert iter.next().name().equals(cname3); + assert iter.next().name().equals(cname4); + assert iter.next().name().equals(cname5); + } + + private void addColumn(RowMutation rm, ByteBuffer cname) + { + rm.add(new QueryPath(cfName, null , cname), ByteBufferUtil.EMPTY_BYTE_BUFFER, 0); + } + + private ByteBuffer createDynamicCompositeKey(String s, UUID uuid, int i, boolean lastIsOne) + { + ByteBuffer bytes = ByteBufferUtil.bytes(s); + int totalSize = 0; + if (s != null) + { + totalSize += 2 + 2 + bytes.remaining() + 1; + if (uuid != null) + { + totalSize += 2 + 2 + 16 + 1; + if (i != -1) + { + totalSize += 2 + "IntegerType".length() + 2 + 1 + 1; + } + } + } + + ByteBuffer bb = ByteBuffer.allocate(totalSize); + + if (s != null) + { + bb.putShort((short)(0x8000 | 'b')); + bb.putShort((short) bytes.remaining()); + bb.put(bytes); + bb.put(uuid == null && lastIsOne ? (byte)1 : (byte)0); + if (uuid != null) + { + bb.putShort((short)(0x8000 | 't')); + bb.putShort((short) 16); + bb.put(UUIDGen.decompose(uuid)); + bb.put(i == -1 && lastIsOne ? (byte)1 : (byte)0); + if (i != -1) + { + bb.putShort((short) "IntegerType".length()); + bb.put(ByteBufferUtil.bytes("IntegerType")); + // We are putting a byte only because our test use ints that fit in a byte *and* IntegerType.fromString() will + // return something compatible (i.e, putting a full int here would break 'fromStringTest') + bb.putShort((short) 1); + bb.put((byte)i); + bb.put(lastIsOne ? (byte)1 : (byte)0); + } + } + } + bb.rewind(); + return bb; + } +}