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

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


The following commit(s) were added to refs/heads/cassandra-4.1 by this push:
     new d25adb498a upsert with adder support is not consistent with numbers 
and strings in LWT
d25adb498a is described below

commit d25adb498abde240cc162cfe4a9630c01381c7f9
Author: David Capwell <[email protected]>
AuthorDate: Tue Sep 6 16:47:04 2022 -0700

    upsert with adder support is not consistent with numbers and strings in LWT
    
    patch by David Capwell; reviewed by Benedict Elliott Smith, Caleb Rackliffe 
for CASSANDRA-17857
---
 CHANGES.txt                                        |   1 +
 src/java/org/apache/cassandra/cql3/Constants.java  |  15 +--
 .../cassandra/distributed/test/CASAddTest.java     |  43 +++++++
 .../cassandra/distributed/test/TestBaseImpl.java   |  10 ++
 .../cql3/validation/operations/UpdateTest.java     |  12 ++
 .../org/apache/cassandra/utils/AssertionUtils.java | 124 +++++++++++++++++++++
 .../apache/cassandra/utils/AssertionUtilsTest.java |  45 ++++++++
 7 files changed, 241 insertions(+), 9 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 900df894c0..1bf090a50f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1-alpha2
+ * upsert with adder support is not consistent with numbers and strings in LWT 
(CASSANDRA-17857)
  * Fix race and return after failing connections (CASSANDRA-17618)
  * Speculative execution threshold unit mismatch (CASSANDRA-17877)
  * Fix BulkLoader to load entireSSTableThrottle and 
entireSSTableInterDcThrottle (CASSANDRA-17677)
diff --git a/src/java/org/apache/cassandra/cql3/Constants.java 
b/src/java/org/apache/cassandra/cql3/Constants.java
index e8989ad24e..64d9d6917e 100644
--- a/src/java/org/apache/cassandra/cql3/Constants.java
+++ b/src/java/org/apache/cassandra/cql3/Constants.java
@@ -485,6 +485,8 @@ public abstract class Constants
                 @SuppressWarnings("unchecked") NumberType<Number> type = 
(NumberType<Number>) column.type;
                 ByteBuffer increment = t.bindAndGet(params.options);
                 ByteBuffer current = getCurrentCellBuffer(partitionKey, 
params);
+                if (current == null)
+                    return;
                 ByteBuffer newValue = type.add(type, current, type, increment);
                 params.addCell(column, newValue);
             }
@@ -494,15 +496,10 @@ public abstract class Constants
                 ByteBuffer current = getCurrentCellBuffer(partitionKey, 
params);
                 ByteBuffer newValue;
                 if (current == null)
-                {
-                    newValue = append;
-                }
-                else
-                {
-                    newValue = ByteBuffer.allocate(current.remaining() + 
append.remaining());
-                    FastByteOperations.copy(current, current.position(), 
newValue, newValue.position(), current.remaining());
-                    FastByteOperations.copy(append, append.position(), 
newValue, newValue.position() + current.remaining(), append.remaining());
-                }
+                    return;
+                newValue = ByteBuffer.allocate(current.remaining() + 
append.remaining());
+                FastByteOperations.copy(current, current.position(), newValue, 
newValue.position(), current.remaining());
+                FastByteOperations.copy(append, append.position(), newValue, 
newValue.position() + current.remaining(), append.remaining());
                 params.addCell(column, newValue);
             }
         }
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java 
b/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java
index 59220cc24d..c50b9b526e 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/CASAddTest.java
@@ -24,6 +24,9 @@ import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.utils.AssertionUtils;
+import org.assertj.core.api.Assertions;
 
 import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
 import static org.apache.cassandra.distributed.shared.AssertUtils.row;
@@ -62,6 +65,46 @@ public class CASAddTest extends TestBaseImpl
         }
     }
 
+    @Test
+    public void testAdditionNotExists() throws Throwable
+    {
+        try (Cluster cluster = init(Cluster.create(3)))
+        {
+            cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int 
PRIMARY KEY, a int, b text)");
+
+            // in this context partition/row not existing looks like column 
not existing, so to simplify the LWT required
+            // condition, add a row with null columns so can rely on IF EXISTS
+            cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl 
(pk) VALUES (1)", ConsistencyLevel.QUORUM);
+
+            // n = n + value where n = null
+            cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET a 
= a + 1, b = b + 'fail' WHERE pk = 1 IF EXISTS", ConsistencyLevel.QUORUM);
+            // the SET should all no-op due to null... so should no-op
+            assertRows(cluster.coordinator(1).execute("SELECT * FROM " + 
KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL), row(1, null, null));
+
+            // this section is testing current limitations... if they start to 
fail due to the limitations going away... update this test to include those 
cases
+            Assertions.assertThatThrownBy(() -> 
cluster.coordinator(1).execute(batch(
+                      "INSERT INTO " + KEYSPACE + ".tbl (pk, a, b) VALUES (1, 
0, '') IF NOT EXISTS",
+                      "UPDATE " + KEYSPACE + ".tbl SET a = a + 1, b = b + 
'success' WHERE pk = 1 IF EXISTS"
+                      ), ConsistencyLevel.QUORUM))
+                      .is(AssertionUtils.is(InvalidRequestException.class))
+                      .hasMessage("Cannot mix IF EXISTS and IF NOT EXISTS 
conditions for the same row");
+            Assertions.assertThatThrownBy(() -> 
cluster.coordinator(1).execute(batch(
+                      "INSERT INTO " + KEYSPACE + ".tbl (pk, a, b) VALUES (1, 
0, '') IF NOT EXISTS",
+
+                      "UPDATE " + KEYSPACE + ".tbl SET a = a + 1, b = b + 
'success' WHERE pk = 1"
+                      ), ConsistencyLevel.QUORUM))
+                      .is(AssertionUtils.is(InvalidRequestException.class))
+                      .hasMessage("Invalid operation (a = a + 1) for non 
counter column a");
+
+            // since CAS doesn't allow the above cases, manually add the data 
to unblock...
+            cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl 
(pk, a, b) VALUES (1, 0, '')", ConsistencyLevel.QUORUM);
+
+            // have cas add defaults when missing
+            cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET a 
= a + 1, b = b + 'success' WHERE pk = 1 IF EXISTS", ConsistencyLevel.QUORUM);
+            assertRows(cluster.coordinator(1).execute("SELECT * FROM " + 
KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL), row(1, 1, "success"));
+        }
+    }
+
     @Test
     public void testConcat() throws Throwable
     {
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java 
b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
index 4755d70f8c..e97a08105f 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java
@@ -104,6 +104,16 @@ public class TestBaseImpl extends DistributedTestBase
         return TupleType.buildValue(bbs);
     }
 
+    public static String batch(String... queries)
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append("BEGIN UNLOGGED BATCH\n");
+        for (String q : queries)
+            sb.append(q).append(";\n");
+        sb.append("APPLY BATCH;");
+        return sb.toString();
+    }
+
     protected void bootstrapAndJoinNode(Cluster cluster)
     {
         IInstanceConfig config = cluster.newInstanceConfig();
diff --git 
a/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java 
b/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java
index 027955780b..59a0616106 100644
--- a/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java
+++ b/test/unit/org/apache/cassandra/cql3/validation/operations/UpdateTest.java
@@ -28,6 +28,8 @@ import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.cql3.UntypedResultSet.Row;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.assertj.core.api.Assertions;
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
@@ -657,4 +659,14 @@ public class UpdateTest extends CQLTester
         ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(currentTable());
         return cfs.metric.allMemtablesLiveDataSize.getValue() == 0;
     }
+
+    @Test
+    public void testAdderNonCounter()
+    {
+        createTable("CREATE TABLE %s (pk int PRIMARY KEY, a int, b text)");
+        Assertions.assertThatThrownBy(() -> execute("UPDATE %s SET a = a + 1, 
b = b + 'fail' WHERE pk = 1"))
+                  .isInstanceOf(InvalidRequestException.class)
+                  // if error ever includes "b" its safe to update this test
+                  .hasMessage("Invalid operation (a = a + 1) for non counter 
column a");
+    }
 }
diff --git a/test/unit/org/apache/cassandra/utils/AssertionUtils.java 
b/test/unit/org/apache/cassandra/utils/AssertionUtils.java
new file mode 100644
index 0000000000..d5b1981fc1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/AssertionUtils.java
@@ -0,0 +1,124 @@
+/*
+ * 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 com.google.common.base.Throwables;
+
+import org.assertj.core.api.Condition;
+
+public class AssertionUtils
+{
+    private AssertionUtils()
+    {
+    }
+
+    /**
+     * When working with jvm-dtest the thrown error is in a different {@link 
ClassLoader} causing type checks
+     * to fail; this method relies on naming instead.
+     */
+    public static Condition<Object> is(Class<?> klass)
+    {
+        String name = klass.getCanonicalName();
+        return new Condition<Object>() {
+            @Override
+            public boolean matches(Object value)
+            {
+                return value.getClass().getCanonicalName().equals(name);
+            }
+
+            @Override
+            public String toString()
+            {
+                return name;
+            }
+        };
+    }
+
+    public static <T extends Throwable> Condition<Throwable> 
isThrowable(Class<T> klass)
+    {
+        // org.assertj.core.api.AbstractAssert.is has <? super ? extends 
Throwable> which blocks <T>, so need to
+        // always return Throwable
+        return (Condition<Throwable>) (Condition<?>) is(klass);
+    }
+
+    /**
+     * When working with jvm-dtest the thrown error is in a different {@link 
ClassLoader} causing type checks
+     * to fail; this method relies on naming instead.
+     *
+     * This method is different than {@link #is(Class)} as it tries to mimic 
instanceOf rather than equality.
+     */
+    public static Condition<Object> isInstanceof(Class<?> klass)
+    {
+        String name = klass.getCanonicalName();
+        return new Condition<Object>() {
+            @Override
+            public boolean matches(Object value)
+            {
+                if (value == null)
+                    return false;
+                return matches(value.getClass());
+            }
+
+            private boolean matches(Class<?> input)
+            {
+                for (Class<?> klass = input; klass != null; klass = 
klass.getSuperclass())
+                {
+                    // extends
+                    if (klass.getCanonicalName().equals(name))
+                        return true;
+                    // implements
+                    for (Class<?> i : klass.getInterfaces())
+                    {
+                        if (matches(i))
+                            return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public String toString()
+            {
+                return name;
+            }
+        };
+    }
+
+    public static Condition<Throwable> rootCause(Condition<Throwable> other)
+    {
+        return new Condition<Throwable>() {
+            @Override
+            public boolean matches(Throwable value)
+            {
+                return other.matches(Throwables.getRootCause(value));
+            }
+
+            @Override
+            public String toString()
+            {
+                return "Root cause " + other;
+            }
+        };
+    }
+
+    public static Condition<Throwable> rootCauseIs(Class<? extends Throwable> 
klass)
+    {
+        return rootCause((Condition<Throwable>) (Condition<?>) is(klass));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/utils/AssertionUtilsTest.java 
b/test/unit/org/apache/cassandra/utils/AssertionUtilsTest.java
new file mode 100644
index 0000000000..e3ec93ab48
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/AssertionUtilsTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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 org.junit.Test;
+
+import org.assertj.core.api.Assertions;
+
+public class AssertionUtilsTest
+{
+    @Test
+    public void isInstanceof()
+    {
+        Assertions.assertThat(new C())
+                  .is(AssertionUtils.isInstanceof(A.class));
+
+        Assertions.assertThat(new D())
+                  .is(AssertionUtils.isInstanceof(A.class))
+                  .is(AssertionUtils.isInstanceof(B.class));
+
+        Assertions.assertThat(null instanceof A)
+                  
.isEqualTo(AssertionUtils.isInstanceof(A.class).matches(null));
+    }
+
+    interface A {}
+    interface B extends A {}
+    static class C implements A {}
+    static class D implements B {}
+}
\ No newline at end of file


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

Reply via email to