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 <calebrackli...@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-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org