Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 8178B200B89 for ; Wed, 21 Sep 2016 16:29:20 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id 7FFC2160ABC; Wed, 21 Sep 2016 14:29:20 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id F1848160ADE for ; Wed, 21 Sep 2016 16:29:18 +0200 (CEST) Received: (qmail 7507 invoked by uid 500); 21 Sep 2016 14:29:17 -0000 Mailing-List: contact commits-help@cassandra.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@cassandra.apache.org Delivered-To: mailing list commits@cassandra.apache.org Received: (qmail 7383 invoked by uid 99); 21 Sep 2016 14:29:17 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 21 Sep 2016 14:29:17 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 72356DFB86; Wed, 21 Sep 2016 14:29:17 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: slebresne@apache.org To: commits@cassandra.apache.org Date: Wed, 21 Sep 2016 14:29:19 -0000 Message-Id: <598a585ac1864cff8567c84b1e6e5e6e@git.apache.org> In-Reply-To: <50e8e520141946c7b0fde8006610e5ec@git.apache.org> References: <50e8e520141946c7b0fde8006610e5ec@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [3/4] cassandra git commit: Establish consistent distinction between non-existing partition and NULL value for LWTs on static columns archived-at: Wed, 21 Sep 2016 14:29:20 -0000 Establish consistent distinction between non-existing partition and NULL value for LWTs on static columns patch by Alex Petrov; reviewed by Sylvain Lebresne for CASSANDRA-12060 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/ead27d9e Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/ead27d9e Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/ead27d9e Branch: refs/heads/cassandra-3.0 Commit: ead27d9e664726da7695fa11ea7e022c11ee7590 Parents: ec60487 Author: Alex Petrov Authored: Wed Aug 10 13:15:30 2016 +0200 Committer: Sylvain Lebresne Committed: Wed Sep 21 16:27:32 2016 +0200 ---------------------------------------------------------------------- CHANGES.txt | 1 + .../cql3/statements/CQL3CasRequest.java | 113 ++++++--- .../operations/InsertUpdateIfConditionTest.java | 249 +++++++++++++------ 3 files changed, 254 insertions(+), 109 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/ead27d9e/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index da56e00..abffd80 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 3.0.10 + * Establish consistent distinction between non-existing partition and NULL value for LWTs on static columns (CASSANDRA-12060) * Extend ColumnIdentifier.internedInstances key to include the type that generated the byte buffer (CASSANDRA-12516) * Backport CASSANDRA-10756 (race condition in NativeTransportService shutdown) (CASSANDRA-12472) * If CF has no clustering columns, any row cache is full partition cache (CASSANDRA-12499) http://git-wip-us.apache.org/repos/asf/cassandra/blob/ead27d9e/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java index 9564005..cf4110c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java @@ -26,8 +26,8 @@ import com.google.common.collect.Multimap; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.cql3.*; import org.apache.cassandra.db.*; -import org.apache.cassandra.db.filter.ClusteringIndexSliceFilter; -import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.filter.*; +import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.partitions.FilteredPartition; import org.apache.cassandra.db.partitions.Partition; import org.apache.cassandra.db.partitions.PartitionUpdate; @@ -48,10 +48,13 @@ public class CQL3CasRequest implements CASRequest private final boolean updatesStaticRow; private boolean hasExists; // whether we have an exist or if not exist condition + // Conditions on the static row. We keep it separate from 'conditions' as most things related to the static row are + // special cases anyway. + private RowCondition staticConditions; // We index RowCondition by the clustering of the row they applied to for 2 reasons: - // 1) this allows to keep things sorted to build the ColumnSlice array below + // 1) this allows to keep things sorted to build the read command below // 2) this allows to detect when contradictory conditions are set (not exists with some other conditions on the same row) - private final SortedMap conditions; + private final TreeMap conditions; private final List updates = new ArrayList<>(); @@ -78,34 +81,37 @@ public class CQL3CasRequest implements CASRequest public void addNotExist(Clustering clustering) throws InvalidRequestException { - RowCondition previous = conditions.put(clustering, new NotExistCondition(clustering)); - if (previous != null && !(previous instanceof NotExistCondition)) - { - // these should be prevented by the parser, but it doesn't hurt to check - if (previous instanceof ExistCondition) - throw new InvalidRequestException("Cannot mix IF EXISTS and IF NOT EXISTS conditions for the same row"); - else - throw new InvalidRequestException("Cannot mix IF conditions and IF NOT EXISTS for the same row"); - } - hasExists = true; + addExistsCondition(clustering, new NotExistCondition(clustering), true); } public void addExist(Clustering clustering) throws InvalidRequestException { - RowCondition previous = conditions.put(clustering, new ExistCondition(clustering)); - // this should be prevented by the parser, but it doesn't hurt to check - if (previous instanceof NotExistCondition) - throw new InvalidRequestException("Cannot mix IF EXISTS and IF NOT EXISTS conditions for the same row"); + addExistsCondition(clustering, new ExistCondition(clustering), false); + } + + private void addExistsCondition(Clustering clustering, RowCondition condition, boolean isNotExist) + { + assert condition instanceof ExistCondition || condition instanceof NotExistCondition; + RowCondition previous = getConditionsForRow(clustering); + if (previous != null && !(previous.getClass().equals(condition.getClass()))) + { + // these should be prevented by the parser, but it doesn't hurt to check + throw (previous instanceof NotExistCondition || previous instanceof ExistCondition) + ? new InvalidRequestException("Cannot mix IF EXISTS and IF NOT EXISTS conditions for the same row") + : new InvalidRequestException("Cannot mix IF conditions and IF " + (isNotExist ? "NOT " : "") + "EXISTS for the same row"); + } + + setConditionsForRow(clustering, condition); hasExists = true; } public void addConditions(Clustering clustering, Collection conds, QueryOptions options) throws InvalidRequestException { - RowCondition condition = conditions.get(clustering); + RowCondition condition = getConditionsForRow(clustering); if (condition == null) { condition = new ColumnsConditions(clustering); - conditions.put(clustering, condition); + setConditionsForRow(clustering, condition); } else if (!(condition instanceof ColumnsConditions)) { @@ -114,6 +120,25 @@ public class CQL3CasRequest implements CASRequest ((ColumnsConditions)condition).addConditions(conds, options); } + private RowCondition getConditionsForRow(Clustering clustering) + { + return clustering == Clustering.STATIC_CLUSTERING ? staticConditions : conditions.get(clustering); + } + + private void setConditionsForRow(Clustering clustering, RowCondition condition) + { + if (clustering == Clustering.STATIC_CLUSTERING) + { + assert staticConditions == null; + staticConditions = condition; + } + else + { + RowCondition previous = conditions.put(clustering, condition); + assert previous == null; + } + } + private PartitionColumns columnsToRead() { // If all our conditions are columns conditions (IF x = ?), then it's enough to query @@ -135,23 +160,37 @@ public class CQL3CasRequest implements CASRequest public SinglePartitionReadCommand readCommand(int nowInSec) { - assert !conditions.isEmpty(); - Slices.Builder builder = new Slices.Builder(cfm.comparator, conditions.size()); - // We always read CQL rows entirely as on CAS failure we want to be able to distinguish between "row exists - // but all values for which there were conditions are null" and "row doesn't exists", and we can't rely on the - // row marker for that (see #6623) - for (Clustering clustering : conditions.keySet()) - { - if (clustering != Clustering.STATIC_CLUSTERING) - builder.add(Slice.make(clustering)); - } - - ClusteringIndexSliceFilter filter = new ClusteringIndexSliceFilter(builder.build(), false); + assert staticConditions != null || !conditions.isEmpty(); + + // With only a static condition, we still want to make the distinction between a non-existing partition and one + // that exists (has some live data) but has not static content. So we query the first live row of the partition. + if (conditions.isEmpty()) + return SinglePartitionReadCommand.create(cfm, + nowInSec, + ColumnFilter.selection(columnsToRead()), + RowFilter.NONE, + DataLimits.cqlLimits(1), + key, + new ClusteringIndexSliceFilter(Slices.ALL, false)); + + ClusteringIndexNamesFilter filter = new ClusteringIndexNamesFilter(conditions.navigableKeySet(), false); return SinglePartitionReadCommand.create(cfm, nowInSec, key, ColumnFilter.selection(columnsToRead()), filter); } + /** + * Checks whether the conditions represented by this object applies provided the current state of the partition on + * which those conditions are. + * + * @param current the partition with current data corresponding to these conditions. More precisely, this must be + * the result of executing the command returned by {@link #readCommand}. This can be empty but it should not be + * {@code null}. + * @return whether the conditions represented by this object applies or not. + */ public boolean appliesTo(FilteredPartition current) throws InvalidRequestException { + if (staticConditions != null && !staticConditions.appliesTo(current)) + return false; + for (RowCondition condition : conditions.values()) { if (!condition.appliesTo(current)) @@ -232,7 +271,7 @@ public class CQL3CasRequest implements CASRequest public boolean appliesTo(FilteredPartition current) { - return current == null || current.getRow(clustering) == null; + return current.getRow(clustering) == null; } } @@ -245,7 +284,7 @@ public class CQL3CasRequest implements CASRequest public boolean appliesTo(FilteredPartition current) { - return current != null && current.getRow(clustering) != null; + return current.getRow(clustering) != null; } } @@ -269,12 +308,10 @@ public class CQL3CasRequest implements CASRequest public boolean appliesTo(FilteredPartition current) throws InvalidRequestException { - if (current == null) - return conditions.isEmpty(); - + Row row = current.getRow(clustering); for (ColumnCondition.Bound condition : conditions.values()) { - if (!condition.appliesTo(current.getRow(clustering))) + if (!condition.appliesTo(row)) return false; } return true; http://git-wip-us.apache.org/repos/asf/cassandra/blob/ead27d9e/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java index a1ee4f8..352100e 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java @@ -111,7 +111,8 @@ public class InsertUpdateIfConditionTest extends CQLTester // Shouldn't apply assertRows(execute("UPDATE %s SET v1 = 3, v2 = 'bar' WHERE k = 0 IF EXISTS"), row(false)); - // Should apply + // Shouldn't apply + assertEmpty(execute("SELECT * FROM %s WHERE k = 0")); assertRows(execute("DELETE FROM %s WHERE k = 0 IF v1 IN (null)"), row(true)); createTable(" CREATE TABLE %s (k int, c int, v1 text, PRIMARY KEY(k, c))"); @@ -1038,14 +1039,20 @@ public class InsertUpdateIfConditionTest extends CQLTester assertRows(execute("SELECT * FROM %s WHERE a = 6"), row(6, 6, 6, "a")); + execute("INSERT INTO %s (a, b, s, d) values (7, 7, 100, 'a')"); + assertRows(execute("UPDATE %s SET s = 7 WHERE a = 7 IF s = 101"), + row(false, 100)); + assertRows(execute("SELECT * FROM %s WHERE a = 7"), + row(7, 7, 100, "a")); + // pre-existing row with null in the static column execute("INSERT INTO %s (a, b, d) values (7, 7, 'a')"); assertRows(execute("UPDATE %s SET s = 7 WHERE a = 7 IF s = NULL"), - row(true)); + row(false, 100)); assertRows(execute("SELECT * FROM %s WHERE a = 7"), - row(7, 7, 7, "a")); + row(7, 7, 100, "a")); - // deleting row before CAS + // deleting row before CAS makes it effectively non-existing execute("DELETE FROM %s WHERE a = 8;"); assertRows(execute("UPDATE %s SET s = 8 WHERE a = 8 IF s = NULL"), row(true)); @@ -1054,70 +1061,151 @@ public class InsertUpdateIfConditionTest extends CQLTester } @Test - public void testConditionalUpdatesWithNullValues() throws Throwable + public void testConditionalUpdatesWithNonExistingValues() throws Throwable { createTable("CREATE TABLE %s (a int, b int, s int static, d text, PRIMARY KEY (a, b))"); - // pre-populate, leave out static column - for (int i = 1; i <= 5; i++) - execute("INSERT INTO %s (a, b) VALUES (?, ?)", i, i); + assertRows(execute("UPDATE %s SET s = 1 WHERE a = 1 IF s = NULL"), + row(true)); + assertRows(execute("SELECT a, s, d FROM %s WHERE a = 1"), + row(1, 1, null)); - conditionalUpdatesWithNonExistingOrNullValues(); + assertRows(execute("UPDATE %s SET s = 2 WHERE a = 2 IF s IN (10,20,NULL)"), + row(true)); + assertRows(execute("SELECT a, s, d FROM %s WHERE a = 2"), + row(2, 2, null)); + + assertRows(execute("UPDATE %s SET s = 4 WHERE a = 4 IF s != 4"), + row(true)); + assertRows(execute("SELECT a, s, d FROM %s WHERE a = 4"), + row(4, 4, null)); // rejected: IN doesn't contain null - assertRows(execute("UPDATE %s SET s = 30 WHERE a = 3 IF s IN (10,20,30)"), + assertRows(execute("UPDATE %s SET s = 3 WHERE a = 3 IF s IN (10,20,30)"), row(false)); - assertRows(execute("SELECT * FROM %s WHERE a = 3"), - row(3, 3, null, null)); + assertEmpty(execute("SELECT a, s, d FROM %s WHERE a = 3")); // rejected: comparing number with NULL always returns false - for (String operator: new String[] { ">", "<", ">=", "<=", "="}) + for (String operator : new String[]{ ">", "<", ">=", "<=", "=" }) { assertRows(execute("UPDATE %s SET s = 50 WHERE a = 5 IF s " + operator + " 3"), row(false)); - assertRows(execute("SELECT * FROM %s WHERE a = 5"), - row(5, 5, null, null)); + assertEmpty(execute("SELECT * FROM %s WHERE a = 5")); } - } @Test - public void testConditionalUpdatesWithNonExistingValues() throws Throwable + public void testConditionalUpdatesWithNullValues() throws Throwable { - createTable("CREATE TABLE %s (a int, b int, s int static, d text, PRIMARY KEY (a, b))"); + createTable("CREATE TABLE %s (a int, b int, s int static, d int, PRIMARY KEY (a, b))"); + + // pre-populate, leave out static column + for (int i = 1; i <= 5; i++) + { + execute("INSERT INTO %s (a, b) VALUES (?, ?)", i, 1); + execute("INSERT INTO %s (a, b) VALUES (?, ?)", i, 2); + } - conditionalUpdatesWithNonExistingOrNullValues(); + assertRows(execute("UPDATE %s SET s = 100 WHERE a = 1 IF s = NULL"), + row(true)); + assertRows(execute("SELECT a, b, s, d FROM %s WHERE a = 1"), + row(1, 1, 100, null), + row(1, 2, 100, null)); + + assertRows(execute("UPDATE %s SET s = 200 WHERE a = 2 IF s IN (10,20,NULL)"), + row(true)); + assertRows(execute("SELECT a, b, s, d FROM %s WHERE a = 2"), + row(2, 1, 200, null), + row(2, 2, 200, null)); // rejected: IN doesn't contain null - assertRows(execute("UPDATE %s SET s = 3 WHERE a = 3 IF s IN (10,20,30)"), - row(false)); - assertEmpty(execute("SELECT a, s, d FROM %s WHERE a = 3")); + assertRows(execute("UPDATE %s SET s = 30 WHERE a = 3 IF s IN (10,20,30)"), + row(false, null)); + assertRows(execute("SELECT * FROM %s WHERE a = 3"), + row(3, 1, null, null), + row(3, 2, null, null)); + + assertRows(execute("UPDATE %s SET s = 400 WHERE a = 4 IF s IN (10,20,NULL)"), + row(true)); + assertRows(execute("SELECT * FROM %s WHERE a = 4"), + row(4, 1, 400, null), + row(4, 2, 400, null)); // rejected: comparing number with NULL always returns false - for (String operator : new String[]{ ">", "<", ">=", "<=", "=" }) + for (String operator: new String[] { ">", "<", ">=", "<=", "="}) { assertRows(execute("UPDATE %s SET s = 50 WHERE a = 5 IF s " + operator + " 3"), - row(false)); - assertEmpty(execute("SELECT * FROM %s WHERE a = 5")); + row(false, null)); + assertRows(execute("SELECT * FROM %s WHERE a = 5"), + row(5, 1, null, null), + row(5, 2, null, null)); } + + assertRows(execute("UPDATE %s SET s = 500 WHERE a = 5 IF s != 5"), + row(true)); + assertRows(execute("SELECT a, b, s, d FROM %s WHERE a = 5"), + row(5, 1, 500, null), + row(5, 2, 500, null)); + + // Similar test, although with two static columns to test limits + createTable("CREATE TABLE %s (a int, b int, s1 int static, s2 int static, d int, PRIMARY KEY (a, b))"); + + for (int i = 1; i <= 5; i++) + for (int j = 0; j < 5; j++) + execute("INSERT INTO %s (a, b, d) VALUES (?, ?, ?)", i, j, i + j); + + assertRows(execute("UPDATE %s SET s2 = 100 WHERE a = 1 IF s1 = NULL"), + row(true)); + + execute("INSERT INTO %s (a, b, s1) VALUES (?, ?, ?)", 2, 2, 2); + assertRows(execute("UPDATE %s SET s1 = 100 WHERE a = 2 IF s2 = NULL"), + row(true)); + + execute("INSERT INTO %s (a, b, s1) VALUES (?, ?, ?)", 2, 2, 2); + assertRows(execute("UPDATE %s SET s1 = 100 WHERE a = 2 IF s2 = NULL"), + row(true)); } - private void conditionalUpdatesWithNonExistingOrNullValues() throws Throwable + @Test + public void testStaticsWithMultipleConditions() throws Throwable { - assertRows(execute("UPDATE %s SET s = 1 WHERE a = 1 IF s = NULL"), - row(true)); - assertRows(execute("SELECT a, s, d FROM %s WHERE a = 1"), - row(1, 1, null)); + createTable("CREATE TABLE %s (a int, b int, s1 int static, s2 int static, d int, PRIMARY KEY (a, b))"); - assertRows(execute("UPDATE %s SET s = 2 WHERE a = 2 IF s IN (10,20,NULL)"), + for (int i = 1; i <= 5; i++) + { + execute("INSERT INTO %s (a, b, d) VALUES (?, ?, ?)", i, 1, 5); + execute("INSERT INTO %s (a, b, d) VALUES (?, ?, ?)", i, 2, 6); + } + + assertRows(execute("BEGIN BATCH\n" + + "UPDATE %1$s SET s2 = 102 WHERE a = 1 IF s1 = null;\n" + + "UPDATE %1$s SET s1 = 101 WHERE a = 1 IF s2 = null;\n" + + "APPLY BATCH"), row(true)); - assertRows(execute("SELECT a, s, d FROM %s WHERE a = 2"), - row(2, 2, null)); + assertRows(execute("SELECT * FROM %s WHERE a = 1"), + row(1, 1, 101, 102, 5), + row(1, 2, 101, 102, 6)); - assertRows(execute("UPDATE %s SET s = 4 WHERE a = 4 IF s != 4"), + + assertRows(execute("BEGIN BATCH\n" + + "UPDATE %1$s SET s2 = 202 WHERE a = 2 IF s1 = null;\n" + + "UPDATE %1$s SET s1 = 201 WHERE a = 2 IF s2 = null;\n" + + "UPDATE %1$s SET d = 203 WHERE a = 2 AND b = 1 IF d = 5;\n" + + "UPDATE %1$s SET d = 204 WHERE a = 2 AND b = 2 IF d = 6;\n" + + "APPLY BATCH"), row(true)); - assertRows(execute("SELECT a, s, d FROM %s WHERE a = 4"), - row(4, 4, null)); + + assertRows(execute("SELECT * FROM %s WHERE a = 2"), + row(2, 1, 201, 202, 203), + row(2, 2, 201, 202, 204)); + + assertRows(execute("BEGIN BATCH\n" + + "UPDATE %1$s SET s2 = 202 WHERE a = 20 IF s1 = null;\n" + + "UPDATE %1$s SET s1 = 201 WHERE a = 20 IF s2 = null;\n" + + "UPDATE %1$s SET d = 203 WHERE a = 20 AND b = 1 IF d = 5;\n" + + "UPDATE %1$s SET d = 204 WHERE a = 20 AND b = 2 IF d = 6;\n" + + "APPLY BATCH"), + row(false)); } @Test @@ -1129,7 +1217,14 @@ public class InsertUpdateIfConditionTest extends CQLTester for (int i = 1; i <= 6; i++) execute("INSERT INTO %s (a, b) VALUES (?, ?)", i, i); - testConditionalUpdatesWithNonExistingOrNullValuesWithBatch(); + // applied: null is indistiguishable from empty value, lwt condition is executed before INSERT + assertRows(execute("BEGIN BATCH\n" + + "INSERT INTO %1$s (a, b, d) values (2, 2, 'a');\n" + + "UPDATE %1$s SET s = 2 WHERE a = 2 IF s = null;\n" + + "APPLY BATCH"), + row(true)); + assertRows(execute("SELECT * FROM %s WHERE a = 2"), + row(2, 2, 2, "a")); // rejected: comparing number with null value always returns false for (String operator: new String[] { ">", "<", ">=", "<=", "="}) @@ -1138,20 +1233,36 @@ public class InsertUpdateIfConditionTest extends CQLTester + "INSERT INTO %1$s (a, b, s, d) values (3, 3, 40, 'a');\n" + "UPDATE %1$s SET s = 30 WHERE a = 3 IF s " + operator + " 5;\n" + "APPLY BATCH"), - row(false)); + row(false, 3, 3, null)); assertRows(execute("SELECT * FROM %s WHERE a = 3"), row(3, 3, null, null)); } + // applied: lwt condition is executed before INSERT, update is applied after it + assertRows(execute("BEGIN BATCH\n" + + "INSERT INTO %1$s (a, b, s, d) values (4, 4, 4, 'a');\n" + + "UPDATE %1$s SET s = 5 WHERE a = 4 IF s = null;\n" + + "APPLY BATCH"), + row(true)); + assertRows(execute("SELECT * FROM %s WHERE a = 4"), + row(4, 4, 5, "a")); + + assertRows(execute("BEGIN BATCH\n" + + "INSERT INTO %1$s (a, b, s, d) values (5, 5, 5, 'a');\n" + + "UPDATE %1$s SET s = 6 WHERE a = 5 IF s IN (1,2,null);\n" + + "APPLY BATCH"), + row(true)); + assertRows(execute("SELECT * FROM %s WHERE a = 5"), + row(5, 5, 6, "a")); + // rejected: IN doesn't contain null assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s, d) values (6, 6, 70, 'a');\n" + "UPDATE %1$s SET s = 60 WHERE a = 6 IF s IN (1,2,3);\n" + "APPLY BATCH"), - row(false)); + row(false, 6, 6, null)); assertRows(execute("SELECT * FROM %s WHERE a = 6"), row(6, 6, null, null)); - } @Test @@ -1159,31 +1270,6 @@ public class InsertUpdateIfConditionTest extends CQLTester { createTable("CREATE TABLE %s (a int, b int, s int static, d text, PRIMARY KEY (a, b))"); - testConditionalUpdatesWithNonExistingOrNullValuesWithBatch(); - - // rejected: comparing number with non-existing value always returns false - for (String operator: new String[] { ">", "<", ">=", "<=", "="}) - { - assertRows(execute("BEGIN BATCH\n" - + "INSERT INTO %1$s (a, b, s, d) values (3, 3, 3, 'a');\n" - + "UPDATE %1$s SET s = 3 WHERE a = 3 IF s " + operator + " 5;\n" - + "APPLY BATCH"), - row(false)); - assertEmpty(execute("SELECT * FROM %s WHERE a = 3")); - } - - // rejected: IN doesn't contain null - assertRows(execute("BEGIN BATCH\n" - + "INSERT INTO %1$s (a, b, s, d) values (6, 6, 6, 'a');\n" - + "UPDATE %1$s SET s = 7 WHERE a = 6 IF s IN (1,2,3);\n" - + "APPLY BATCH"), - row(false)); - assertEmpty(execute("SELECT * FROM %s WHERE a = 6")); - } - - private void testConditionalUpdatesWithNonExistingOrNullValuesWithBatch() throws Throwable - { - // applied: null is indistiguishable from empty value, lwt condition is executed before INSERT assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, d) values (2, 2, 'a');\n" + "UPDATE %1$s SET s = 2 WHERE a = 2 IF s = null;\n" @@ -1199,7 +1285,7 @@ public class InsertUpdateIfConditionTest extends CQLTester + "APPLY BATCH"), row(true)); assertRows(execute("SELECT * FROM %s WHERE a = 4"), - row(4, 4, 5, "a")); + row(4, 4, 5, "a")); // Note that the update wins because 5 > 4 (we have a timestamp tie, so values are used) assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s, d) values (5, 5, 5, 'a');\n" @@ -1207,7 +1293,7 @@ public class InsertUpdateIfConditionTest extends CQLTester + "APPLY BATCH"), row(true)); assertRows(execute("SELECT * FROM %s WHERE a = 5"), - row(5, 5, 6, "a")); + row(5, 5, 6, "a")); // Same as above assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s, d) values (7, 7, 7, 'a');\n" @@ -1215,7 +1301,26 @@ public class InsertUpdateIfConditionTest extends CQLTester + "APPLY BATCH"), row(true)); assertRows(execute("SELECT * FROM %s WHERE a = 7"), - row(7, 7, 8, "a")); + row(7, 7, 8, "a")); // Same as above + + // rejected: comparing number with non-existing value always returns false + for (String operator: new String[] { ">", "<", ">=", "<=", "="}) + { + assertRows(execute("BEGIN BATCH\n" + + "INSERT INTO %1$s (a, b, s, d) values (3, 3, 3, 'a');\n" + + "UPDATE %1$s SET s = 3 WHERE a = 3 IF s " + operator + " 5;\n" + + "APPLY BATCH"), + row(false)); + assertEmpty(execute("SELECT * FROM %s WHERE a = 3")); + } + + // rejected: IN doesn't contain null + assertRows(execute("BEGIN BATCH\n" + + "INSERT INTO %1$s (a, b, s, d) values (6, 6, 6, 'a');\n" + + "UPDATE %1$s SET s = 7 WHERE a = 6 IF s IN (1,2,3);\n" + + "APPLY BATCH"), + row(false)); + assertEmpty(execute("SELECT * FROM %s WHERE a = 6")); } @Test @@ -1233,7 +1338,7 @@ public class InsertUpdateIfConditionTest extends CQLTester // rejected: IN doesn't contain null assertRows(execute("DELETE s1 FROM %s WHERE a = 2 IF s2 IN (10,20,30)"), - row(false)); + row(false, null)); assertRows(execute("SELECT * FROM %s WHERE a = 2"), row(2, 2, 2, null, 2)); @@ -1251,7 +1356,7 @@ public class InsertUpdateIfConditionTest extends CQLTester for (String operator : new String[]{ ">", "<", ">=", "<=", "=" }) { assertRows(execute("DELETE s1 FROM %s WHERE a = 5 IF s2 " + operator + " 3"), - row(false)); + row(false, null)); assertRows(execute("SELECT * FROM %s WHERE a = 5"), row(5, 5, 5, null, 5)); } @@ -1262,7 +1367,6 @@ public class InsertUpdateIfConditionTest extends CQLTester { createTable("CREATE TABLE %s (a int, b int, s1 int static, s2 int static, v int, PRIMARY KEY (a, b))"); - // applied: null is indistiguishable from empty value, lwt condition is executed before INSERT assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s1, v) values (2, 2, 2, 2);\n" + "DELETE s1 FROM %1$s WHERE a = 2 IF s2 = null;\n" @@ -1290,6 +1394,7 @@ public class InsertUpdateIfConditionTest extends CQLTester row(false)); assertEmpty(execute("SELECT * FROM %s WHERE a = 6")); + // Note that on equal timestamp, tombstone wins so the DELETE wins assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s1, v) values (4, 4, 4, 4);\n" + "DELETE s1 FROM %1$s WHERE a = 4 IF s2 = null;\n" @@ -1298,6 +1403,7 @@ public class InsertUpdateIfConditionTest extends CQLTester assertRows(execute("SELECT * FROM %s WHERE a = 4"), row(4, 4, null, null, 4)); + // Note that on equal timestamp, tombstone wins so the DELETE wins assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s1, v) VALUES (5, 5, 5, 5);\n" + "DELETE s1 FROM %1$s WHERE a = 5 IF s1 IN (1,2,null);\n" @@ -1306,6 +1412,7 @@ public class InsertUpdateIfConditionTest extends CQLTester assertRows(execute("SELECT * FROM %s WHERE a = 5"), row(5, 5, null, null, 5)); + // Note that on equal timestamp, tombstone wins so the DELETE wins assertRows(execute("BEGIN BATCH\n" + "INSERT INTO %1$s (a, b, s1, v) values (7, 7, 7, 7);\n" + "DELETE s1 FROM %1$s WHERE a = 7 IF s2 != 7;\n"