From d80bb323c9dd8f814fe02e16dec510557c7d5101 Mon Sep 17 00:00:00 2001 From: ROCHETEAU Antoine Date: Thu, 6 Jan 2022 13:46:50 +0100 Subject: [PATCH] CONTAINS and CONTAINS KEY support for Lightweight Transactions patch by ROCHETEAU Antoine; reviewed by Benjamin Lerer, Berenguer Blasi for CASSANDRA-10537 --- CHANGES.txt | 1 + .../cassandra/pages/cql/cql_singlefile.adoc | 2 + pylib/cqlshlib/cql3handling.py | 2 +- pylib/cqlshlib/test/test_cqlsh_completion.py | 19 +- src/antlr/Parser.g | 1 + .../org/apache/cassandra/cql3/Operator.java | 18 ++ .../cql3/conditions/ColumnCondition.java | 42 ++++ .../cql3/conditions/ColumnConditionTest.java | 61 ++++++ ...nsertUpdateIfConditionCollectionsTest.java | 207 ++++++++++++++++-- .../InsertUpdateIfConditionTest.java | 1 + 10 files changed, 331 insertions(+), 23 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 414164181d..0f60d18244 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.1 + * Add support for CONTAINS and CONTAINS KEY in conditional UPDATE and DELETE statement (CASSANDRA-10537) * Migrate advanced config parameters to the new Config types (CASSANDRA-17431) * Make null to be meaning disabled and leave 0 as a valid value for permissions_update_interval, roles_update_interval, credentials_update_interval (CASSANDRA-17431) * Fix typo in Config annotation (CASSANDRA-17431) diff --git a/doc/modules/cassandra/pages/cql/cql_singlefile.adoc b/doc/modules/cassandra/pages/cql/cql_singlefile.adoc index 56541e0231..4fe8c10a65 100644 --- a/doc/modules/cassandra/pages/cql/cql_singlefile.adoc +++ b/doc/modules/cassandra/pages/cql/cql_singlefile.adoc @@ -1386,6 +1386,7 @@ WHERE + | `.' `=' ::= + +| CONTAINS (KEY)? + | IN + | `[' `]' + | `[' `]' IN + @@ -1498,6 +1499,7 @@ WHERE + ::= ( | `(' ( ( `,' )* )? `)') ::= ( | `!=') + +| CONTAINS (KEY)? + | IN + | `[' `]' ( | `!=') + | `[' `]' IN + diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 4025a3767d..0e50efbf23 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -956,7 +956,7 @@ syntax_rules += r''' ; ::= ( "AND" )* ; - ::= (("=" | "<" | ">" | "<=" | ">=" | "!=") ) + ::= (("=" | "<" | ">" | "<=" | ">=" | "!=" | "CONTAINS" ( "KEY" )? ) ) | ("IN" "(" ( "," )* ")" ) ; ::= conditioncol= diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py index 696028a283..06f7664712 100644 --- a/pylib/cqlshlib/test/test_cqlsh_completion.py +++ b/pylib/cqlshlib/test/test_cqlsh_completion.py @@ -381,7 +381,16 @@ class TestCqlshCompletion(CqlshCompletionCase): choices=['EXISTS', '', '']) self.trycompletions("UPDATE empty_table SET lonelycol = 'eggs' WHERE TOKEN(lonelykey) <= TOKEN(13) IF EXISTS ", - choices=['>=', '!=', '<=', 'IN', '[', ';', '=', '<', '>', '.']) + choices=['>=', '!=', '<=', 'IN', '[', ';', '=', '<', '>', '.', 'CONTAINS']) + + self.trycompletions("UPDATE empty_table SET lonelycol = 'eggs' WHERE TOKEN(lonelykey) <= TOKEN(13) IF lonelykey ", + choices=['>=', '!=', '<=', 'IN', '=', '<', '>', 'CONTAINS']) + + self.trycompletions("UPDATE empty_table SET lonelycol = 'eggs' WHERE TOKEN(lonelykey) <= TOKEN(13) IF lonelykey CONTAINS ", + choices=['false', 'true', '', + '-', '', 'TOKEN', '', + '', '{', '[', 'NULL', '', + '', '', 'KEY']) def test_complete_in_delete(self): self.trycompletions('DELETE F', choices=['FROM', '', '']) @@ -463,7 +472,13 @@ class TestCqlshCompletion(CqlshCompletionCase): choices=['EXISTS', '', '']) self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE ' 'TOKEN(a) >= TOKEN(0) IF b '), - choices=['>=', '!=', '<=', 'IN', '=', '<', '>']) + choices=['>=', '!=', '<=', 'IN', '=', '<', '>', 'CONTAINS']) + self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE ' + 'TOKEN(a) >= TOKEN(0) IF b CONTAINS '), + choices=['false', 'true', '', + '-', '', 'TOKEN', '', + '', '{', '[', 'NULL','', + '','', 'KEY']) self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE ' 'TOKEN(a) >= TOKEN(0) IF b < 0 '), choices=['AND', ';']) diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index 27a01f3d3e..dbb2682617 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -1662,6 +1662,7 @@ columnCondition[List> conditions] // Note: we'll reject duplicates later : key=cident ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleCondition(t, op))); } + | op=containsOperator t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleCondition(t, op))); } | K_IN ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleInCondition(values))); } | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.simpleInCondition(marker))); } diff --git a/src/java/org/apache/cassandra/cql3/Operator.java b/src/java/org/apache/cassandra/cql3/Operator.java index 992056ceb9..bcb5f63be6 100644 --- a/src/java/org/apache/cassandra/cql3/Operator.java +++ b/src/java/org/apache/cassandra/cql3/Operator.java @@ -340,4 +340,22 @@ public enum Operator { return this == IN; } + + /** + * Checks if this operator is CONTAINS operator. + * @return {@code true} if this operator is a CONTAINS operator, {@code false} otherwise. + */ + public boolean isContains() + { + return this == CONTAINS; + } + + /** + * Checks if this operator is CONTAINS KEY operator. + * @return {@code true} if this operator is a CONTAINS operator, {@code false} otherwise. + */ + public boolean isContainsKey() + { + return this == CONTAINS_KEY; + } } diff --git a/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java b/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java index d27fd3fd92..e3f463a255 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java +++ b/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java @@ -495,6 +495,9 @@ public abstract class ColumnCondition if (value == null) return !iter.hasNext(); + if(operator.isContains() || operator.isContainsKey()) + return containsAppliesTo(type, iter, value.get(ProtocolVersion.CURRENT), operator); + switch (type.kind) { case LIST: @@ -573,6 +576,39 @@ public abstract class ColumnCondition } } + private static boolean containsAppliesTo(CollectionType type, Iterator> iter, ByteBuffer value, Operator operator) + { + AbstractType compareType; + switch (type.kind) + { + case LIST: + compareType = ((ListType)type).getElementsType(); + break; + case SET: + compareType = ((SetType)type).getElementsType(); + break; + case MAP: + compareType = operator.isContainsKey() ? ((MapType)type).getKeysType() : ((MapType)type).getValuesType(); + break; + default: + throw new AssertionError(); + } + boolean appliesToSetOrMapKeys = (type.kind == CollectionType.Kind.SET || type.kind == CollectionType.Kind.MAP && operator.isContainsKey()); + return containsAppliesTo(compareType, iter, value, appliesToSetOrMapKeys); + } + + private static boolean containsAppliesTo(AbstractType type, Iterator> iter, ByteBuffer value, Boolean appliesToSetOrMapKeys) + { + while(iter.hasNext()) + { + // for lists and map values we use the cell value; for sets and map keys we use the cell name + ByteBuffer cellValue = appliesToSetOrMapKeys ? iter.next().path().get(0) : iter.next().buffer(); + if(type.compare(cellValue, value) == 0) + return true; + } + return false; + } + /** * A condition on a UDT field */ @@ -819,12 +855,18 @@ public abstract class ColumnCondition private Terms prepareTerms(String keyspace, ColumnSpecification receiver) { + checkFalse(operator.isContainsKey() && !(receiver.type instanceof MapType), "Cannot use CONTAINS KEY on non-map column %s", receiver.name); + checkFalse(operator.isContains() && !(receiver.type.isCollection()), "Cannot use CONTAINS on non-collection column %s", receiver.name); + if (operator.isIN()) { return inValues == null ? Terms.ofListMarker(inMarker.prepare(keyspace, receiver), receiver.type) : Terms.of(prepareTerms(keyspace, receiver, inValues)); } + if (operator.isContains() || operator.isContainsKey()) + receiver = ((CollectionType) receiver.type).makeCollectionReceiver(receiver, operator.isContainsKey()); + return Terms.of(value.prepare(keyspace, receiver)); } diff --git a/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java b/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java index 7a7e4b8d16..a896125430 100644 --- a/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java @@ -135,6 +135,22 @@ public class ColumnConditionTest return bound.appliesTo(newRow(definition, rowValue)); } + private static boolean conditionContainsApplies(List rowValue, Operator op, ByteBuffer conditionValue) + { + ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", ListType.getInstance(Int32Type.instance, true)); + ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(new Constants.Value(conditionValue))); + ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); + return bound.appliesTo(newRow(definition, rowValue)); + } + + private static boolean conditionContainsApplies(Map rowValue, Operator op, ByteBuffer conditionValue) + { + ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", MapType.getInstance(Int32Type.instance, Int32Type.instance, true)); + ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(new Constants.Value(conditionValue))); + ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); + return bound.appliesTo(newRow(definition, rowValue)); + } + private static boolean conditionApplies(SortedSet rowValue, Operator op, SortedSet conditionValue) { ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", SetType.getInstance(Int32Type.instance, true)); @@ -143,6 +159,14 @@ public class ColumnConditionTest return bound.appliesTo(newRow(definition, rowValue)); } + private static boolean conditionContainsApplies(SortedSet rowValue, Operator op, ByteBuffer conditionValue) + { + ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", SetType.getInstance(Int32Type.instance, true)); + ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(new Constants.Value(conditionValue))); + ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); + return bound.appliesTo(newRow(definition, rowValue)); + } + private static boolean conditionApplies(Map rowValue, Operator op, Map conditionValue) { ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", MapType.getInstance(Int32Type.instance, Int32Type.instance, true)); @@ -327,6 +351,14 @@ public class ColumnConditionTest assertTrue(conditionApplies(list(ONE), GTE, list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); assertFalse(conditionApplies(list(ByteBufferUtil.EMPTY_BYTE_BUFFER), GTE, list(ONE))); assertTrue(conditionApplies(list(ByteBufferUtil.EMPTY_BYTE_BUFFER), GTE, list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); + + //CONTAINS + assertTrue(conditionContainsApplies(list(ZERO, ONE, TWO), CONTAINS, ONE)); + assertFalse(conditionContainsApplies(list(ZERO, ONE), CONTAINS, TWO)); + + assertFalse(conditionContainsApplies(list(ZERO, ONE, TWO), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + assertFalse(conditionContainsApplies(list(ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS, ONE)); + assertTrue(conditionContainsApplies(list(ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); } private static SortedSet set(ByteBuffer... values) @@ -422,6 +454,14 @@ public class ColumnConditionTest assertTrue(conditionApplies(set(ONE), GTE, set(ByteBufferUtil.EMPTY_BYTE_BUFFER))); assertFalse(conditionApplies(set(ByteBufferUtil.EMPTY_BYTE_BUFFER), GTE, set(ONE))); assertTrue(conditionApplies(set(ByteBufferUtil.EMPTY_BYTE_BUFFER), GTE, set(ByteBufferUtil.EMPTY_BYTE_BUFFER))); + + // CONTAINS + assertTrue(conditionContainsApplies(set(ZERO, ONE, TWO), CONTAINS, ONE)); + assertFalse(conditionContainsApplies(set(ZERO, ONE), CONTAINS, TWO)); + + assertFalse(conditionContainsApplies(set(ZERO, ONE, TWO), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + assertFalse(conditionContainsApplies(set(ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS, ONE)); + assertTrue(conditionContainsApplies(set(ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); } // values should be a list of key, value, key, value, ... @@ -550,5 +590,26 @@ public class ColumnConditionTest assertFalse(conditionApplies(map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), GTE, map(ONE, ONE))); assertTrue(conditionApplies(map(ByteBufferUtil.EMPTY_BYTE_BUFFER, ONE), GTE, map(ByteBufferUtil.EMPTY_BYTE_BUFFER, ONE))); assertTrue(conditionApplies(map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), GTE, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER))); + + //CONTAINS + assertTrue(conditionContainsApplies(map(ZERO, ONE), CONTAINS, ONE)); + assertFalse(conditionContainsApplies(map(ZERO, ONE), CONTAINS, ZERO)); + + assertFalse(conditionContainsApplies(map(ONE, ONE), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + assertTrue(conditionContainsApplies(map(ByteBufferUtil.EMPTY_BYTE_BUFFER, ONE), CONTAINS, ONE)); + assertFalse(conditionContainsApplies(map(ByteBufferUtil.EMPTY_BYTE_BUFFER, ONE), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + assertFalse(conditionContainsApplies(map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS, ONE)); + assertTrue(conditionContainsApplies(map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + + //CONTAINS KEY + assertTrue(conditionContainsApplies(map(ZERO, ONE), CONTAINS_KEY, ZERO)); + assertFalse(conditionContainsApplies(map(ZERO, ONE), CONTAINS_KEY, ONE)); + + assertFalse(conditionContainsApplies(map(ONE, ONE), CONTAINS_KEY, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + assertFalse(conditionContainsApplies(map(ByteBufferUtil.EMPTY_BYTE_BUFFER, ONE), CONTAINS_KEY, ONE)); + assertTrue(conditionContainsApplies(map(ByteBufferUtil.EMPTY_BYTE_BUFFER, ONE), CONTAINS_KEY, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + assertTrue(conditionContainsApplies(map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS_KEY, ONE)); + assertFalse(conditionContainsApplies(map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), CONTAINS_KEY, ByteBufferUtil.EMPTY_BYTE_BUFFER)); + } } diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java index 7ad41e3084..c0a5139b5c 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java @@ -154,8 +154,8 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester checkInvalidUDT("v >= null", v, InvalidRequestException.class); checkInvalidUDT("v IN null", v, SyntaxException.class); checkInvalidUDT("v IN 367", v, SyntaxException.class); - checkInvalidUDT("v CONTAINS KEY 123", v, SyntaxException.class); - checkInvalidUDT("v CONTAINS 'bar'", v, SyntaxException.class); + checkInvalidUDT("v CONTAINS KEY 123", v, InvalidRequestException.class); + checkInvalidUDT("v CONTAINS 'bar'", v, InvalidRequestException.class); /////////////////// null suffix on stored udt //////////////////// v = userType("a", 0, "b", null); @@ -433,21 +433,35 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester void checkAppliesUDT(String condition, Object value) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value), row(true)); assertRows(execute("SELECT * FROM %s"), row(0, value)); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k = 0 IF " + condition), row(true)); + assertEmpty(execute("SELECT * FROM %s")); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", value); } void checkDoesNotApplyUDT(String condition, Object value) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value), row(false, value)); assertRows(execute("SELECT * FROM %s"), row(0, value)); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k = 0 IF " + condition), + row(false, value)); + assertRows(execute("SELECT * FROM %s"), row(0, value)); } void checkInvalidUDT(String condition, Object value, Class expected) throws Throwable { + // UPDATE statement assertInvalidThrow(expected, "UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value); assertRows(execute("SELECT * FROM %s"), row(0, value)); + // DELETE statement + assertInvalidThrow(expected, "DELETE FROM %s WHERE k = 0 IF " + condition); + assertRows(execute("SELECT * FROM %s"), row(0, value)); } /** @@ -472,10 +486,12 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_applies_list("l < ['z']"); check_applies_list("l <= ['z']"); check_applies_list("l IN (null, ['foo', 'bar', 'foobar'], ['a'])"); + check_applies_list("l CONTAINS 'bar'"); // multiple conditions check_applies_list("l > ['aaa', 'bbb'] AND l > ['aaa']"); check_applies_list("l != null AND l IN (['foo', 'bar', 'foobar'])"); + check_applies_list("l CONTAINS 'foo' AND l CONTAINS 'foobar'"); // should not apply check_does_not_apply_list("l = ['baz']"); @@ -486,10 +502,12 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_does_not_apply_list("l <= ['a']"); check_does_not_apply_list("l IN (['a'], null)"); check_does_not_apply_list("l IN ()"); + check_does_not_apply_list("l CONTAINS 'baz'"); // multiple conditions check_does_not_apply_list("l IN () AND l IN (['foo', 'bar', 'foobar'])"); check_does_not_apply_list("l > ['zzz'] AND l < ['zzz']"); + check_does_not_apply_list("l CONTAINS 'bar' AND l CONTAINS 'baz'"); check_invalid_list("l = [null]", InvalidRequestException.class); check_invalid_list("l < null", InvalidRequestException.class); @@ -498,30 +516,42 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_invalid_list("l >= null", InvalidRequestException.class); check_invalid_list("l IN null", SyntaxException.class); check_invalid_list("l IN 367", SyntaxException.class); - check_invalid_list("l CONTAINS KEY 123", SyntaxException.class); - - // not supported yet - check_invalid_list("m CONTAINS 'bar'", SyntaxException.class); + check_invalid_list("l CONTAINS KEY 123", InvalidRequestException.class); + check_invalid_list("l CONTAINS null", InvalidRequestException.class); } } void check_applies_list(String condition) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET l = ['foo', 'bar', 'foobar'] WHERE k=0 IF " + condition), row(true)); assertRows(execute("SELECT * FROM %s"), row(0, list("foo", "bar", "foobar"))); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k=0 IF " + condition), row(true)); + assertEmpty(execute("SELECT * FROM %s")); + execute("INSERT INTO %s(k, l) VALUES (0, ['foo', 'bar', 'foobar'])"); } void check_does_not_apply_list(String condition) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET l = ['foo', 'bar', 'foobar'] WHERE k=0 IF " + condition), row(false, list("foo", "bar", "foobar"))); assertRows(execute("SELECT * FROM %s"), row(0, list("foo", "bar", "foobar"))); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k=0 IF " + condition), + row(false, list("foo", "bar", "foobar"))); + assertRows(execute("SELECT * FROM %s"), row(0, list("foo", "bar", "foobar"))); } void check_invalid_list(String condition, Class expected) throws Throwable { + // UPDATE statement assertInvalidThrow(expected, "UPDATE %s SET l = ['foo', 'bar', 'foobar'] WHERE k=0 IF " + condition); assertRows(execute("SELECT * FROM %s"), row(0, list("foo", "bar", "foobar"))); + // DELETE statement + assertInvalidThrow(expected, "DELETE FROM %s WHERE k=0 IF " + condition); + assertRows(execute("SELECT * FROM %s"), row(0, list("foo", "bar", "foobar"))); } /** @@ -543,7 +573,8 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester "DELETE FROM %s WHERE k=0 IF l[?] = ?", null, "foobar"); assertInvalidMessage("Invalid negative list index -2", "DELETE FROM %s WHERE k=0 IF l[?] = ?", -2, "foobar"); - + assertInvalidSyntax("DELETE FROM %s WHERE k=0 IF l[?] CONTAINS ?", 0, "bar"); + assertInvalidSyntax("DELETE FROM %s WHERE k=0 IF l[?] CONTAINS KEY ?", 0, "bar"); assertRows(execute("DELETE FROM %s WHERE k=0 IF l[?] = ?", 1, null), row(false, list("foo", "bar", "foobar"))); assertRows(execute("DELETE FROM %s WHERE k=0 IF l[?] = ?", 1, "foobar"), row(false, list("foo", "bar", "foobar"))); assertRows(execute("SELECT * FROM %s"), row(0, list("foo", "bar", "foobar"))); @@ -631,10 +662,12 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_applies_set("s < {'z'}"); check_applies_set("s <= {'z'}"); check_applies_set("s IN (null, {'bar', 'foo'}, {'a'})"); + check_applies_set("s CONTAINS 'foo'"); // multiple conditions check_applies_set("s > {'a'} AND s < {'z'}"); check_applies_set("s IN (null, {'bar', 'foo'}, {'a'}) AND s IN ({'a'}, {'bar', 'foo'}, null)"); + check_applies_set("s CONTAINS 'foo' AND s CONTAINS 'bar'"); // should not apply check_does_not_apply_set("s = {'baz'}"); @@ -646,6 +679,7 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_does_not_apply_set("s IN ({'a'}, null)"); check_does_not_apply_set("s IN ()"); check_does_not_apply_set("s != null AND s IN ()"); + check_does_not_apply_set("s CONTAINS 'baz'"); check_invalid_set("s = {null}", InvalidRequestException.class); check_invalid_set("s < null", InvalidRequestException.class); @@ -654,32 +688,43 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_invalid_set("s >= null", InvalidRequestException.class); check_invalid_set("s IN null", SyntaxException.class); check_invalid_set("s IN 367", SyntaxException.class); - check_invalid_set("s CONTAINS KEY 123", SyntaxException.class); + check_invalid_set("s CONTAINS null", InvalidRequestException.class); + check_invalid_set("s CONTAINS KEY 123", InvalidRequestException.class); // element access is not allow for sets check_invalid_set("s['foo'] = 'foobar'", InvalidRequestException.class); - - // not supported yet - check_invalid_set("m CONTAINS 'bar'", SyntaxException.class); } } void check_applies_set(String condition) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET s = {'bar', 'foo'} WHERE k=0 IF " + condition), row(true)); assertRows(execute("SELECT * FROM %s"), row(0, set("bar", "foo"))); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k=0 IF " + condition), row(true)); + assertEmpty(execute("SELECT * FROM %s")); + execute("INSERT INTO %s (k, s) VALUES (0, {'bar', 'foo'})"); } void check_does_not_apply_set(String condition) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET s = {'bar', 'foo'} WHERE k=0 IF " + condition), row(false, set("bar", "foo"))); assertRows(execute("SELECT * FROM %s"), row(0, set("bar", "foo"))); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k=0 IF " + condition), row(false, set("bar", "foo"))); + assertRows(execute("SELECT * FROM %s"), row(0, set("bar", "foo"))); } void check_invalid_set(String condition, Class expected) throws Throwable { + // UPDATE statement assertInvalidThrow(expected, "UPDATE %s SET s = {'bar', 'foo'} WHERE k=0 IF " + condition); assertRows(execute("SELECT * FROM %s"), row(0, set("bar", "foo"))); + // DELETE statement + assertInvalidThrow(expected, "DELETE FROM %s WHERE k=0 IF " + condition); + assertRows(execute("SELECT * FROM %s"), row(0, set("bar", "foo"))); } /** @@ -704,10 +749,13 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_applies_map("m <= {'z': 'z'}"); check_applies_map("m != {'a': 'a'}"); check_applies_map("m IN (null, {'a': 'a'}, {'foo': 'bar'})"); + check_applies_map("m CONTAINS 'bar'"); + check_applies_map("m CONTAINS KEY 'foo'"); // multiple conditions check_applies_map("m > {'a': 'a'} AND m < {'z': 'z'}"); check_applies_map("m != null AND m IN (null, {'a': 'a'}, {'foo': 'bar'})"); + check_applies_map("m CONTAINS 'bar' AND m CONTAINS KEY 'foo'"); // should not apply check_does_not_apply_map("m = {'a': 'a'}"); @@ -719,18 +767,16 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester check_does_not_apply_map("m IN ({'a': 'a'}, null)"); check_does_not_apply_map("m IN ()"); check_does_not_apply_map("m = null AND m != null"); + check_does_not_apply_map("m CONTAINS 'foo'"); + check_does_not_apply_map("m CONTAINS KEY 'bar'"); check_invalid_map("m = {null: null}", InvalidRequestException.class); check_invalid_map("m = {'a': null}", InvalidRequestException.class); check_invalid_map("m = {null: 'a'}", InvalidRequestException.class); + check_invalid_map("m CONTAINS null", InvalidRequestException.class); + check_invalid_map("m CONTAINS KEY null", InvalidRequestException.class); check_invalid_map("m < null", InvalidRequestException.class); check_invalid_map("m IN null", SyntaxException.class); - - // not supported yet - check_invalid_map("m CONTAINS 'bar'", SyntaxException.class); - check_invalid_map("m CONTAINS KEY 'foo'", SyntaxException.class); - check_invalid_map("m CONTAINS null", SyntaxException.class); - check_invalid_map("m CONTAINS KEY null", SyntaxException.class); } } @@ -750,6 +796,8 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester execute("INSERT INTO %s (k, m) VALUES (0, {'foo' : 'bar'})"); assertInvalidMessage("Invalid null value for map element access", "DELETE FROM %s WHERE k=0 IF m[?] = ?", null, "foo"); + assertInvalidSyntax("DELETE FROM %s WHERE k=0 IF m[?] CONTAINS ?", "foo", "bar"); + assertInvalidSyntax("DELETE FROM %s WHERE k=0 IF m[?] CONTAINS KEY ?", "foo", "bar"); assertRows(execute("DELETE FROM %s WHERE k=0 IF m[?] = ?", "foo", "foo"), row(false, map("foo", "bar"))); assertRows(execute("DELETE FROM %s WHERE k=0 IF m[?] = ?", "foo", null), row(false, map("foo", "bar"))); assertRows(execute("SELECT * FROM %s"), row(0, map("foo", "bar"))); @@ -769,17 +817,17 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester @Test public void testFrozenWithNullValues() throws Throwable { - createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, m %s)", "frozen>")); + createTable("CREATE TABLE %s (k int PRIMARY KEY, m frozen>)"); execute("INSERT INTO %s (k, m) VALUES (0, null)"); assertRows(execute("UPDATE %s SET m = ? WHERE k = 0 IF m = ?", list("test"), list("comparison")), row(false, null)); - createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, m %s)", "frozen>")); + createTable("CREATE TABLE %s (k int PRIMARY KEY, m frozen>)"); execute("INSERT INTO %s (k, m) VALUES (0, null)"); assertRows(execute("UPDATE %s SET m = ? WHERE k = 0 IF m = ?", map("test", 3), map("comparison", 2)), row(false, null)); - createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, m %s)", "frozen>")); + createTable("CREATE TABLE %s (k int PRIMARY KEY, m frozen>)"); execute("INSERT INTO %s (k, m) VALUES (0, null)"); assertRows(execute("UPDATE %s SET m = ? WHERE k = 0 IF m = ?", set("test"), set("comparison")), row(false, null)); @@ -838,20 +886,32 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester void check_applies_map(String condition) throws Throwable { + // UPDATE statement assertRows(execute("UPDATE %s SET m = {'foo': 'bar'} WHERE k=0 IF " + condition), row(true)); assertRows(execute("SELECT * FROM %s"), row(0, map("foo", "bar"))); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k=0 IF " + condition), row(true)); + assertEmpty(execute("SELECT * FROM %s")); + execute("INSERT INTO %s (k, m) VALUES (0, {'foo' : 'bar'})"); } void check_does_not_apply_map(String condition) throws Throwable { assertRows(execute("UPDATE %s SET m = {'foo': 'bar'} WHERE k=0 IF " + condition), row(false, map("foo", "bar"))); assertRows(execute("SELECT * FROM %s"), row(0, map("foo", "bar"))); + // DELETE statement + assertRows(execute("DELETE FROM %s WHERE k=0 IF " + condition), row(false, map("foo", "bar"))); + assertRows(execute("SELECT * FROM %s"), row(0, map("foo", "bar"))); } void check_invalid_map(String condition, Class expected) throws Throwable { + // UPDATE statement assertInvalidThrow(expected, "UPDATE %s SET m = {'foo': 'bar'} WHERE k=0 IF " + condition); assertRows(execute("SELECT * FROM %s"), row(0, map("foo", "bar"))); + // DELETE statement + assertInvalidThrow(expected, "DELETE FROM %s WHERE k=0 IF " + condition); + assertRows(execute("SELECT * FROM %s"), row(0, map("foo", "bar"))); } @Test @@ -918,4 +978,111 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester "UPDATE %s SET v = {a: 0, b: 'bc'} WHERE k = 0 IF v.a IN ?", unset()); } } + + @Test + public void testNonFrozenEmptyCollection() throws Throwable + { + createTable("CREATE TABLE %s (k int PRIMARY KEY, l list)"); + execute("INSERT INTO %s (k, l) VALUES (0, null)"); + + // Does apply + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l = ?", (ByteBuffer) null), + row(true)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l = ?", list()), + row(true)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l != ?", list("bar")), + row(true)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l < ?", list("a")), + row(true)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l <= ?", list("a")), + row(true)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l IN (?, ?)", null, list("bar")), + row(true)); + + // Does not apply + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l = ?", list("bar")), + row(false, null)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l > ?", list("a")), + row(false, null)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l >= ?", list("a")), + row(false, null)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l CONTAINS ?", "bar"), + row(false, null)); + assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l CONTAINS ?", unset()), + row(false, null)); + + assertInvalidMessage("Invalid comparison with null for operator \"CONTAINS\"", + "UPDATE %s SET l = null WHERE k = 0 IF l CONTAINS ?", (ByteBuffer) null); + + createTable("CREATE TABLE %s (k int PRIMARY KEY, s set)"); + execute("INSERT INTO %s (k, s) VALUES (0, null)"); + + // Does apply + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s = ?", (ByteBuffer) null), + row(true)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s = ?", set()), + row(true)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s != ?", set("bar")), + row(true)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s < ?", set("a")), + row(true)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s <= ?", set("a")), + row(true)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s IN (?, ?)", null, set("bar")), + row(true)); + + // Does not apply + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s = ?", set("bar")), + row(false, null)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s > ?", set("a")), + row(false, null)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s >= ?", set("a")), + row(false, null)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s CONTAINS ?", "bar"), + row(false, null)); + assertRows(execute("UPDATE %s SET s = null WHERE k = 0 IF s CONTAINS ?", unset()), + row(false, null)); + + assertInvalidMessage("Invalid comparison with null for operator \"CONTAINS\"", + "UPDATE %s SET s = null WHERE k = 0 IF s CONTAINS ?", (ByteBuffer) null); + + createTable("CREATE TABLE %s (k int PRIMARY KEY, m map) "); + execute("INSERT INTO %s (k, m) VALUES (0, null)"); + + // Does apply + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m = ?", (ByteBuffer) null), + row(true)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m = ?", map()), + row(true)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m != ?", map("foo","bar")), + row(true)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m < ?", map("a","a")), + row(true)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m <= ?", map("a","a")), + row(true)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m IN (?, ?)", null, map("foo","bar")), + row(true)); + + // Does not apply + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m = ?", map("foo","bar")), + row(false, null)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m > ?", map("a", "a")), + row(false, null)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m >= ?", map("a", "a")), + row(false, null)); + assertRows(execute("UPDATE %s SET m = null WHERE k = 0 IF m CONTAINS ?", "bar"), + row(false, null)); + assertRows(execute("UPDATE %s SET m = {} WHERE k = 0 IF m CONTAINS ?", unset()), + row(false, null)); + assertRows(execute("UPDATE %s SET m = {} WHERE k = 0 IF m CONTAINS KEY ?", "foo"), + row(false, null)); + assertRows(execute("UPDATE %s SET m = {} WHERE k = 0 IF m CONTAINS KEY ?", unset()), + row(false, null)); + + assertInvalidMessage("Invalid comparison with null for operator \"CONTAINS\"", + "UPDATE %s SET m = {} WHERE k = 0 IF m CONTAINS ?", (ByteBuffer) null); + assertInvalidMessage("Invalid comparison with null for operator \"CONTAINS KEY\"", + "UPDATE %s SET m = {} WHERE k = 0 IF m CONTAINS KEY ?", (ByteBuffer) null); + } + } 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 ac03cb5e14..627075f9de 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java @@ -194,6 +194,7 @@ public class InsertUpdateIfConditionTest extends CQLTester "UPDATE %s SET v1 = 'A' WHERE k = 0 AND c IN () IF EXISTS"); assertInvalidMessage("IN on the clustering key columns is not supported with conditional updates", "UPDATE %s SET v1 = 'A' WHERE k = 0 AND c IN (1, 2) IF EXISTS"); + assertInvalidMessage("Cannot use CONTAINS on non-collection column v1", "UPDATE %s SET v1 = 'B' WHERE k = 0 IF v1 CONTAINS 'A'"); } /**