You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ma...@apache.org on 2025/03/06 17:23:44 UTC
(cassandra) branch cassandra-4.0 updated: Support null column value tombstones in FQL batch statements
This is an automated email from the ASF dual-hosted git repository.
maedhroz pushed a commit to branch cassandra-4.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cassandra-4.0 by this push:
new f375a3914f Support null column value tombstones in FQL batch statements
f375a3914f is described below
commit f375a3914f9e322e11ac30b2ea2999ff14bb65d4
Author: Caleb Rackliffe <ca...@gmail.com>
AuthorDate: Tue Mar 4 18:04:08 2025 -0600
Support null column value tombstones in FQL batch statements
patch by Caleb Rackliffe; reviewed by Abe Ratnofsky for CASSANDRA-20397
---
CHANGES.txt | 1 +
.../org/apache/cassandra/fql/FullQueryLogger.java | 4 +-
.../test/{ => fql}/FqlReplayDDLExclusionTest.java | 3 +-
.../test/fql/FqlTombstoneHandlingTest.java | 115 +++++++++++++++++++++
.../apache/cassandra/fqltool/FQLQueryReader.java | 5 +-
.../apache/cassandra/fqltool/commands/Dump.java | 9 +-
6 files changed, 129 insertions(+), 8 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index fff1039d33..30d253d29b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
4.0.18
+ * Support null column value tombstones in FQL batch statements (CASSANDRA-20397)
* Update Zstd library to 1.5.7-1 (CASSANDRA-20367)
* Fix premature auto-failing of long-running repairs (CASSANDRA-20312)
diff --git a/src/java/org/apache/cassandra/fql/FullQueryLogger.java b/src/java/org/apache/cassandra/fql/FullQueryLogger.java
index 0604df67fb..34c9641e77 100644
--- a/src/java/org/apache/cassandra/fql/FullQueryLogger.java
+++ b/src/java/org/apache/cassandra/fql/FullQueryLogger.java
@@ -424,9 +424,7 @@ public class FullQueryLogger implements QueryEvents.Listener
{
valueOut.int32(subValues.size());
for (ByteBuffer value : subValues)
- {
- valueOut.bytes(BytesStore.wrap(value));
- }
+ valueOut.bytes(value == null ? null : BytesStore.wrap(value));
}
}
diff --git a/test/distributed/org/apache/cassandra/distributed/test/FqlReplayDDLExclusionTest.java b/test/distributed/org/apache/cassandra/distributed/test/fql/FqlReplayDDLExclusionTest.java
similarity index 97%
rename from test/distributed/org/apache/cassandra/distributed/test/FqlReplayDDLExclusionTest.java
rename to test/distributed/org/apache/cassandra/distributed/test/fql/FqlReplayDDLExclusionTest.java
index 3dbef35c78..daa1ae25d6 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/FqlReplayDDLExclusionTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/fql/FqlReplayDDLExclusionTest.java
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-package org.apache.cassandra.distributed.test;
+package org.apache.cassandra.distributed.test.fql;
import org.junit.Ignore;
import org.junit.Rule;
@@ -27,6 +27,7 @@ import com.datastax.driver.core.Session;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.QueryResults;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.tools.ToolRunner;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/fql/FqlTombstoneHandlingTest.java b/test/distributed/org/apache/cassandra/distributed/test/fql/FqlTombstoneHandlingTest.java
new file mode 100644
index 0000000000..45e10ab1b0
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/distributed/test/fql/FqlTombstoneHandlingTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.distributed.test.fql;
+
+import com.google.common.collect.Sets;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.datastax.driver.core.BatchStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.Session;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+import org.apache.cassandra.tools.ToolRunner;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
+import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
+import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+public class FqlTombstoneHandlingTest extends TestBaseImpl
+{
+ private static Cluster CLUSTER;
+
+ @Rule
+ public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @BeforeClass
+ public static void beforeClass() throws Throwable
+ {
+ CLUSTER = init(Cluster.build(1).withConfig(updater -> updater.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)).start());
+ }
+
+ @Test
+ public void testNullCellBindingInBatch()
+ {
+ String tableName = "null_as_tombstone_in_batch";
+ CLUSTER.schemaChange(withKeyspace("CREATE TABLE %s." + tableName + " (k int, c int, s set<int>, primary key (k, c))"));
+ CLUSTER.get(1).nodetool("enablefullquerylog", "--path", temporaryFolder.getRoot().getAbsolutePath());
+ String insertTemplate = withKeyspace("INSERT INTO %s." + tableName + " (k, c, s) VALUES ( ?, ?, ?) USING TIMESTAMP 2");
+ String select = withKeyspace("SELECT * FROM %s." + tableName + " WHERE k = 0 AND c = 0");
+
+ com.datastax.driver.core.Cluster.Builder builder1 =com.datastax.driver.core.Cluster.builder().addContactPoint("127.0.0.1");
+
+ // Use the driver to write this initial row, since otherwise we won't hit the dispatcher
+ try (com.datastax.driver.core.Cluster cluster1 = builder1.build(); Session session1 = cluster1.connect())
+ {
+ BatchStatement batch = new BatchStatement(BatchStatement.Type.UNLOGGED);
+ PreparedStatement preparedWrite = session1.prepare(insertTemplate);
+ batch.add(preparedWrite.bind(0, 0, null));
+ session1.execute(batch);
+ }
+
+ CLUSTER.get(1).nodetool("disablefullquerylog");
+
+ // The dump should contain a null entry for our tombstone
+ ToolRunner.ToolResult runner = ToolRunner.invokeClass("org.apache.cassandra.fqltool.FullQueryLogTool",
+ "dump",
+ "--",
+ temporaryFolder.getRoot().getAbsolutePath());
+ assertTrue(runner.getStdout().contains(insertTemplate));
+ assertEquals(0, runner.getExitCode());
+
+ Object[][] preReplayResult = CLUSTER.get(1).executeInternal(select);
+ assertRows(preReplayResult, row(0, 0, null));
+
+ // Make sure the row no longer exists after truncate...
+ CLUSTER.get(1).executeInternal(withKeyspace("TRUNCATE %s." + tableName));
+ assertRows(CLUSTER.get(1).executeInternal(select));
+
+ // ...insert a new row with an actual value for the set at an earlier timestamp...
+ CLUSTER.get(1).executeInternal(withKeyspace("INSERT INTO %s." + tableName + " (k, c, s) VALUES ( ?, ?, ?) USING TIMESTAMP 1"), 0, 0, Sets.newHashSet(1));
+ assertRows(CLUSTER.get(1).executeInternal(select), row(0, 0, Sets.newHashSet(1)));
+
+ runner = ToolRunner.invokeClass("org.apache.cassandra.fqltool.FullQueryLogTool",
+ "replay",
+ "--keyspace", KEYSPACE,
+ "--target", "127.0.0.1",
+ "--", temporaryFolder.getRoot().getAbsolutePath());
+ assertEquals(0, runner.getExitCode());
+
+ // ...then ensure the replayed row deletes the one we wrote before replay.
+ Object[][] postReplayResult = CLUSTER.get(1).executeInternal(select);
+ assertRows(postReplayResult, preReplayResult);
+ }
+
+ @AfterClass
+ public static void afterClass()
+ {
+ if (CLUSTER != null)
+ CLUSTER.close();
+ }
+}
diff --git a/tools/fqltool/src/org/apache/cassandra/fqltool/FQLQueryReader.java b/tools/fqltool/src/org/apache/cassandra/fqltool/FQLQueryReader.java
index 20f362b2a6..c717a6e5ce 100644
--- a/tools/fqltool/src/org/apache/cassandra/fqltool/FQLQueryReader.java
+++ b/tools/fqltool/src/org/apache/cassandra/fqltool/FQLQueryReader.java
@@ -94,7 +94,10 @@ public class FQLQueryReader implements ReadMarshallable
values.add(subValues);
int numSubValues = in.int32();
for (int zz = 0; zz < numSubValues; zz++)
- subValues.add(ByteBuffer.wrap(in.bytes()));
+ {
+ byte[] valueBytes = in.bytes();
+ subValues.add(valueBytes == null ? null : ByteBuffer.wrap(valueBytes));
+ }
}
query = new FQLQuery.Batch(keyspace,
protocolVersion,
diff --git a/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Dump.java b/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Dump.java
index e954f81933..e383dbc7ba 100644
--- a/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Dump.java
+++ b/tools/fqltool/src/org/apache/cassandra/fqltool/commands/Dump.java
@@ -126,7 +126,7 @@ public class Dump implements Runnable
break;
case (FullQueryLogger.BATCH):
- dumpBatch(options, wireIn, sb);
+ dumpBatch(wireIn, sb);
break;
default:
@@ -183,7 +183,7 @@ public class Dump implements Runnable
sb.append(System.lineSeparator());
}
- private static void dumpBatch(QueryOptions options, WireIn wireIn, StringBuilder sb)
+ private static void dumpBatch(WireIn wireIn, StringBuilder sb)
{
sb.append("Batch type: ")
.append(wireIn.read(FullQueryLogger.BATCH_TYPE).text())
@@ -203,7 +203,10 @@ public class Dump implements Runnable
int numSubValues = in.int32();
List<ByteBuffer> subValues = new ArrayList<>(numSubValues);
for (int j = 0; j < numSubValues; j++)
- subValues.add(ByteBuffer.wrap(in.bytes()));
+ {
+ byte[] valueBytes = in.bytes();
+ subValues.add(valueBytes == null ? null : ByteBuffer.wrap(valueBytes));
+ }
sb.append("Query: ")
.append(queries.get(i))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org