This is an automated email from the ASF dual-hosted git repository.

benedict pushed a commit to branch cassandra-3.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-3.0 by this push:
     new d50ec52  LegacyLayout should handle paging states that cross a 
collection column
d50ec52 is described below

commit d50ec52d6e6c9140080c86fcff08b3cd37a6ca3d
Author: Benedict Elliott Smith <[email protected]>
AuthorDate: Thu Jul 4 12:45:45 2019 +0100

    LegacyLayout should handle paging states that cross a collection column
---
 CHANGES.txt                                        |  1 +
 src/java/org/apache/cassandra/db/LegacyLayout.java | 98 +++++++++++++++++-----
 .../org/apache/cassandra/db/LegacyLayoutTest.java  | 54 ++++++++++++
 3 files changed, 130 insertions(+), 23 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index ef6f49c..878877a 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 3.0.19
+ * LegacyLayout should handle paging states that cross a collection column 
(CASSANDRA-15201)
  * Prevent RuntimeException when username or password is empty/null 
(CASSANDRA-15198)
  * Multiget thrift query returns null records after digest mismatch 
(CASSANDRA-14812)
  * Skipping illegal legacy cells can break reverse iteration of indexed 
partitions (CASSANDRA-15178)
diff --git a/src/java/org/apache/cassandra/db/LegacyLayout.java 
b/src/java/org/apache/cassandra/db/LegacyLayout.java
index b03f56e..a46aa91 100644
--- a/src/java/org/apache/cassandra/db/LegacyLayout.java
+++ b/src/java/org/apache/cassandra/db/LegacyLayout.java
@@ -24,6 +24,7 @@ import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.SuperColumnCompatibility;
@@ -235,36 +236,45 @@ public abstract class LegacyLayout
         assert !isStatic ||
                 (components.size() >= clusteringSize
                         && all(components.subList(0, clusteringSize), 
ByteBufferUtil.EMPTY_BYTE_BUFFER::equals));
+
         ColumnDefinition collectionName = null;
         if (components.size() > clusteringSize)
         {
             // For a deletion, there can be more components than the 
clustering size only in the case this is the
             // bound of a collection range tombstone. In such a case, there is 
exactly one more component, and that
-            // component is the name of the collection being selected/deleted.
-            // If the bound is not part of a deletion, it is from slice query 
filter. In this scnario, the column name
-            // may be a valid, non-collection column or it may be an empty 
buffer, representing a row marker. In either
-            // case, this needn't be included in the returned bound, so we pop 
the last element from the components
-            // list but ensure that the collection name remains null.
-
-            assert clusteringSize + 1 == components.size() && 
!metadata.isCompactTable();
-            // pop the final element from the back of the list of clusterings
-            ByteBuffer columnNameBytes = components.remove(clusteringSize);
-            if (isDeletion)
+            // component is the name of the collection being deleted, since we 
do not support collection range deletions.
+            // If the bound is not part of a deletion, it is from slice query 
filter. The column name may be:
+            //   - a valid, non-collection column; in this case we expect a 
single extra component
+            //   - an empty buffer, representing a row marker; in this case we 
also expect a single extra empty component
+            //   - a valid collection column and the first part of a cell 
path; in this case we expect exactly two extra components
+            // In any of these slice cases, these items are unnecessary for 
the bound we construct,
+            // so we can simply remove them, after corroborating we have 
encountered one of these scenario.
+            assert !metadata.isCompactTable() : toDebugHex(components);
+
+            // In all cases, the element straight after the clusterings should 
contain the name of a column.
+            if (components.size() > clusteringSize + 1)
             {
-                collectionName = metadata.getColumnDefinition(columnNameBytes);
-                if (collectionName == null || !collectionName.isComplex())
-                {
-                    collectionName = 
metadata.getDroppedColumnDefinition(columnNameBytes, isStatic);
-                    // if no record of the column having ever existed is 
found, something is badly wrong
-                    if (collectionName == null)
-                        throw new RuntimeException("Unknown collection column 
" + UTF8Type.instance.getString(columnNameBytes) + " during deserialization");
-
-                    // if we do have a record of dropping this column but it 
wasn't previously complex, use a fake
-                    // column definition for safety (see the comment on the 
constant declaration for details)
-                    if (!collectionName.isComplex())
-                        collectionName = 
INVALID_DROPPED_COMPLEX_SUBSTITUTE_COLUMN;
-                }
+                // we accept bounds from paging state that occur inside a 
complex column - in this case, we expect
+                // two excess components, the first of which is a column name, 
the second a key into the collection
+                if (isDeletion)
+                    throw new IllegalArgumentException("Invalid bound " + 
toDebugHex(components) + ": deletion can have at most one extra component");
+
+                if (clusteringSize + 2 != components.size())
+                    throw new IllegalArgumentException("Invalid bound " + 
toDebugHex(components) + ": complex slices require exactly two extra 
components");
+
+                // decode simply to verify that we have (or may have had) a 
complex column; we assume the collection key is valid
+                decodeBoundLookupComplexColumn(metadata, components, 
clusteringSize, isStatic);
+                components.remove(clusteringSize + 1);
+            }
+            else if (isDeletion)
+            {
+                collectionName = decodeBoundLookupComplexColumn(metadata, 
components, clusteringSize, isStatic);
+            }
+            else if (components.get(clusteringSize).hasRemaining())
+            {
+                decodeBoundVerifySimpleColumn(metadata, components, 
clusteringSize, isStatic);
             }
+            components.remove(clusteringSize);
         }
 
         boolean isInclusive;
@@ -292,6 +302,48 @@ public abstract class LegacyLayout
         return new LegacyBound(sb, isStatic, collectionName);
     }
 
+    // finds the simple column definition associated with 
components.get(clusteringSize)
+    // if no such columns exists, or ever existed, we throw an exception; if 
we do not know, we return a dummy column definition
+    private static ColumnDefinition decodeBoundLookupComplexColumn(CFMetaData 
metadata, List<ByteBuffer> components, int clusteringSize, boolean isStatic)
+    {
+        ByteBuffer columnNameBytes = components.get(clusteringSize);
+        ColumnDefinition columnName = 
metadata.getColumnDefinition(columnNameBytes);
+        if (columnName == null || !columnName.isComplex())
+        {
+            columnName = metadata.getDroppedColumnDefinition(columnNameBytes, 
isStatic);
+            // if no record of the column having ever existed is found, 
something is badly wrong
+            if (columnName == null)
+                throw new IllegalArgumentException("Invalid bound " + 
toDebugHex(components) + ": expected complex column at position " + 
clusteringSize);
+
+            // if we do have a record of dropping this column but it wasn't 
previously complex, use a fake
+            // column definition for safety (see the comment on the constant 
declaration for details)
+            if (!columnName.isComplex())
+                columnName = INVALID_DROPPED_COMPLEX_SUBSTITUTE_COLUMN;
+        }
+
+        return columnName;
+    }
+
+    // finds the simple column definition associated with 
components.get(clusteringSize)
+    // if no such columns exists, and definitely never existed, we throw an 
exception
+    private static void decodeBoundVerifySimpleColumn(CFMetaData metadata, 
List<ByteBuffer> components, int clusteringSize, boolean isStatic)
+    {
+        ByteBuffer columnNameBytes = components.get(clusteringSize);
+        ColumnDefinition columnName = 
metadata.getColumnDefinition(columnNameBytes);
+        if (columnName == null || !columnName.isSimple())
+        {
+            columnName = metadata.getDroppedColumnDefinition(columnNameBytes, 
isStatic);
+            // if no record of the column having ever existed is found, 
something is badly wrong
+            if (columnName == null)
+                throw new IllegalArgumentException("Invalid bound " + 
toDebugHex(components) + ": expected simple column at position " + 
clusteringSize);
+        }
+    }
+
+    private static String toDebugHex(Collection<ByteBuffer> buffers)
+    {
+        return 
buffers.stream().map(ByteBufferUtil::bytesToHex).collect(Collectors.joining());
+    }
+
     public static ByteBuffer encodeBound(CFMetaData metadata, Slice.Bound 
bound, boolean isStart)
     {
         if (bound == Slice.Bound.BOTTOM || bound == Slice.Bound.TOP || 
metadata.comparator.size() == 0)
diff --git a/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java 
b/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java
index e34f58e..580a0e7 100644
--- a/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java
+++ b/test/unit/org/apache/cassandra/db/LegacyLayoutTest.java
@@ -38,6 +38,7 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
 import org.apache.cassandra.net.MessagingService;
 import org.apache.cassandra.serializers.Int32Serializer;
 import org.apache.cassandra.serializers.UTF8Serializer;
+import org.apache.cassandra.service.MigrationManager;
 import org.apache.cassandra.utils.FBUtilities;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -57,6 +58,7 @@ import org.apache.cassandra.db.rows.Row;
 import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.schema.KeyspaceParams;
 import org.apache.cassandra.utils.ByteBufferUtil;
+import org.apache.cassandra.utils.Hex;
 
 import static org.junit.Assert.*;
 
@@ -291,4 +293,56 @@ public class LegacyLayoutTest
         }
     }
 
+    @Test
+    public void testDecodeLegacyPagedRangeCommandSerializer() throws 
IOException
+    {
+        /*
+         Run on 2.1
+         public static void main(String[] args) throws IOException, 
ConfigurationException
+         {
+             Gossiper.instance.start((int) (System.currentTimeMillis() / 
1000));
+             Keyspace.setInitialized();
+             CFMetaData cfMetaData = CFMetaData.sparseCFMetaData("ks", "cf", 
UTF8Type.instance)
+             .addColumnDefinition(new ColumnDefinition("ks", "cf", new 
ColumnIdentifier("v", true), SetType.getInstance(Int32Type.instance, false), 
null, null, null, null, ColumnDefinition.Kind.REGULAR));
+             KSMetaData ksMetaData = KSMetaData.testMetadata("ks", 
SimpleStrategy.class, KSMetaData.optsWithRF(3), cfMetaData);
+             MigrationManager.announceNewKeyspace(ksMetaData);
+             RowPosition position = 
RowPosition.ForKey.get(ByteBufferUtil.EMPTY_BYTE_BUFFER, new 
Murmur3Partitioner());
+             SliceQueryFilter filter = new IdentityQueryFilter();
+             Composite cellName = CellNames.compositeSparseWithCollection(new 
ByteBuffer[0], Int32Type.instance.decompose(1), new ColumnIdentifier("v", 
true), false);
+             try (DataOutputBuffer buffer = new DataOutputBuffer(1024))
+             {
+                 PagedRangeCommand command = new PagedRangeCommand("ks", "cf", 
1, AbstractBounds.bounds(position, true, position, true), filter, cellName, 
filter.finish(), Collections.emptyList(), 1, true);
+                 PagedRangeCommand.serializer.serialize(command, buffer, 
MessagingService.current_version);
+                 System.out.println(Hex.bytesToHex(buffer.toByteArray()));
+             }
+         }
+         */
+
+        DatabaseDescriptor.setDaemonInitialized();
+        Keyspace.setInitialized();
+        CFMetaData table = CFMetaData.Builder.create("ks", "cf")
+                                             .addPartitionKey("k", 
Int32Type.instance)
+                                             .addRegularColumn("v", 
SetType.getInstance(Int32Type.instance, true))
+                                             .build();
+        SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1));
+        MigrationManager.announceNewColumnFamily(table);
+
+        byte[] bytes = 
Hex.hexToBytes("00026b73000263660000000000000001fffffffe01000000088000000000000000010000000880000000000000000000000100000000007fffffffffffffff000b00017600000400000001000000000000000000000101");
+        ReadCommand.legacyPagedRangeCommandSerializer.deserialize(new 
DataInputBuffer(bytes), MessagingService.VERSION_21);
+    }
+
+    @Test
+    public void testDecodeCollectionPageBoundary()
+    {
+        CFMetaData table = CFMetaData.Builder.create("ks", "cf")
+                                             .addPartitionKey("k", 
Int32Type.instance)
+                                             .addRegularColumn("v", 
SetType.getInstance(Int32Type.instance, true))
+                                             .build();
+
+        ColumnDefinition v = table.getColumnDefinition(new 
ColumnIdentifier("v", false));
+        ByteBuffer bound = LegacyLayout.encodeCellName(table, 
Clustering.EMPTY, v.name.bytes, Int32Type.instance.decompose(1));
+
+        LegacyLayout.decodeSliceBound(table, bound, true);
+    }
+
 }
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to