Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions commit_message.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Restrict BytesType compatibility to scalar types only

BytesType.isValueCompatibleWith() previously accepted all types,
including frozen collections and UDTs. This was incorrect because
converting collections or UDTs to raw bytes is nonsensical - it
allows reading raw serialized bytes which have no meaningful
interpretation.

This patch restricts BytesType to only accept simple scalar types
by checking !isCollection() && !isUDT() in isValueCompatibleWithInternal().

patch by Dekrate for CASSANDRA-20982

Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,10 @@ private void addColumn(KeyspaceMetadata keyspace,
// columns is pushed deeper down the line. The latter would still be problematic in cases of schema races.
if (!type.isSerializationCompatibleWith(droppedColumn.type))
{
throw ire("Cannot re-add previously dropped column '%s' of type %s, incompatible with previous type %s",
throw ire("Cannot add a column '%s' of type %s, incompatible with previously dropped column '%s' of type %s",
name,
type.asCQL3Type(),
name,
droppedColumn.type.asCQL3Type());
}

Expand Down
5 changes: 3 additions & 2 deletions src/java/org/apache/cassandra/db/marshal/BytesType.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ public boolean isCompatibleWith(AbstractType<?> previous)
@Override
public boolean isValueCompatibleWithInternal(AbstractType<?> otherType)
{
// BytesType can read anything
return true;
// BytesType should only be compatible with simple scalar types, not with collections or UDTs
// because converting a collection or UDT to raw bytes is nonsensical
return !otherType.isMultiCell();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this here will break cql CAST operations, which I think we still might want to allow since they are explicit, and different from finding a column you re-added contains junk bytes.

Also, isMultiCell will return false for frozen collections.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driftx, should I revert to the first version?

return !otherType.isCollection() && !otherType.isUDT();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and we shouldn't do this in isValueCompatibleWithInternal since it will prevent CASTing to bytes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driftx, thanks! Could you please suggest any particular place to put it in?

To sum up:

  1. isValueCompatibleWithInternal returns true back
  2. return !otherType.isCollection() && !otherType.isUDT(); will be in the other place?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this isSerializationCompatibleWith?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSerializationCompatibleWith will prevent the schema change but still allow the cast.

}

public CQL3Type asCQL3Type()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,30 @@ public void testNonFrozenCollectionsAreIncompatibleWithBlob() throws Throwable
{
createTable("CREATE TABLE %s (a int, b " + type + ", PRIMARY KEY (a));");
alterTable("ALTER TABLE %s DROP b;");
assertInvalidMessage("Cannot re-add previously dropped column 'b' of type blob, incompatible with previous type " + type,
assertInvalidMessage("Cannot add a column 'b' of type blob, incompatible with previously dropped column 'b' of type " + type,
"ALTER TABLE %s ADD b blob;");
}

for (String type : collectionTypes)
{
createTable("CREATE TABLE %s (a int, b blob, PRIMARY KEY (a));");
alterTable("ALTER TABLE %s DROP b;");
assertInvalidMessage("Cannot re-add previously dropped column 'b' of type " + type + ", incompatible with previous type blob",
assertInvalidMessage("Cannot add a column 'b' of type " + type + ", incompatible with previously dropped column 'b' of type blob",
"ALTER TABLE %s ADD b " + type + ';');
}
}

@Test
public void testFrozenCollectionsAreCompatibleWithBlob()
public void testFrozenCollectionsAreNotCompatibleWithBlob()
{
String[] collectionTypes = new String[] {"frozen<map<int, int>>", "frozen<set<int>>", "frozen<list<int>>"};

for (String type : collectionTypes)
{
createTable("CREATE TABLE %s (a int, b " + type + ", PRIMARY KEY (a));");
alterTable("ALTER TABLE %s DROP b;");
alterTable("ALTER TABLE %s ADD b blob;");
assertInvalidMessage("Cannot add a column 'b' of type blob, incompatible with previously dropped column 'b' of type " + type,
"ALTER TABLE %s ADD b blob;");
}
}

Expand Down
71 changes: 71 additions & 0 deletions test/unit/org/apache/cassandra/db/marshal/BytesTypeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@
*/
package org.apache.cassandra.db.marshal;

import java.util.Arrays;

import org.apache.cassandra.cql3.FieldIdentifier;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.junit.Test;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class BytesTypeTest
{
private static final String INVALID_HEX = "33AG45F"; // Invalid (has a G)
Expand All @@ -38,4 +45,68 @@ public void testFromStringWithValidString()
{
BytesType.instance.fromString(VALID_HEX);
}

@Test
public void testValueCompatibilityWithScalarTypes()
{
// BytesType should be compatible with simple scalar types
assertTrue("BytesType should be compatible with AsciiType",
BytesType.instance.isValueCompatibleWith(AsciiType.instance));
assertTrue("BytesType should be compatible with UTF8Type",
BytesType.instance.isValueCompatibleWith(UTF8Type.instance));
assertTrue("BytesType should be compatible with Int32Type",
BytesType.instance.isValueCompatibleWith(Int32Type.instance));
assertTrue("BytesType should be compatible with LongType",
BytesType.instance.isValueCompatibleWith(LongType.instance));
assertTrue("BytesType should be compatible with UUIDType",
BytesType.instance.isValueCompatibleWith(UUIDType.instance));
assertTrue("BytesType should be compatible with BooleanType",
BytesType.instance.isValueCompatibleWith(BooleanType.instance));
assertTrue("BytesType should be compatible with itself",
BytesType.instance.isValueCompatibleWith(BytesType.instance));
}

@Test
public void testValueIncompatibilityWithCollections()
{
// BytesType should NOT be compatible with collections (even frozen ones)
// because converting a collection to raw bytes is nonsensical
ListType<?> frozenList = ListType.getInstance(Int32Type.instance, false);
SetType<?> frozenSet = SetType.getInstance(Int32Type.instance, false);
MapType<?, ?> frozenMap = MapType.getInstance(Int32Type.instance, UTF8Type.instance, false);

assertFalse("BytesType should NOT be compatible with frozen<list>",
BytesType.instance.isValueCompatibleWith(frozenList));
assertFalse("BytesType should NOT be compatible with frozen<set>",
BytesType.instance.isValueCompatibleWith(frozenSet));
assertFalse("BytesType should NOT be compatible with frozen<map>",
BytesType.instance.isValueCompatibleWith(frozenMap));

// Also test with multi-cell collections
ListType<?> multiCellList = ListType.getInstance(Int32Type.instance, true);
SetType<?> multiCellSet = SetType.getInstance(Int32Type.instance, true);
MapType<?, ?> multiCellMap = MapType.getInstance(Int32Type.instance, UTF8Type.instance, true);

assertFalse("BytesType should NOT be compatible with list",
BytesType.instance.isValueCompatibleWith(multiCellList));
assertFalse("BytesType should NOT be compatible with set",
BytesType.instance.isValueCompatibleWith(multiCellSet));
assertFalse("BytesType should NOT be compatible with map",
BytesType.instance.isValueCompatibleWith(multiCellMap));
}

@Test
public void testValueIncompatibilityWithUDT()
{
// BytesType should NOT be compatible with User Defined Types
// because converting a UDT to raw bytes is nonsensical
UserType udt = new UserType("ks",
ByteBufferUtil.bytes("myType"),
Arrays.asList(FieldIdentifier.forQuoted("field1"), FieldIdentifier.forQuoted("field2")),
Arrays.asList(Int32Type.instance, UTF8Type.instance),
true);

assertFalse("BytesType should NOT be compatible with UDT",
BytesType.instance.isValueCompatibleWith(udt));
}
}