diff --git a/CHANGES.txt b/CHANGES.txt index 862ea3e975..e738a2e0da 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2.0.7 + * Add support for DELETE ... IF EXISTS to CQL3 (CASSANDRA-5708) * Update hadoop_cql3_word_count example (CASSANDRA-6793) * Fix handling of RejectedExecution in sync Thrift server (CASSANDRA-6788) * Log more information when exceeding tombstone_warn_threshold (CASSANDRA-6865) diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g index a11a818ea3..ed482d0cd5 100644 --- a/src/java/org/apache/cassandra/cql3/Cql.g +++ b/src/java/org/apache/cassandra/cql3/Cql.g @@ -371,24 +371,27 @@ updateConditions returns [List> cond * DELETE name1, name2 * FROM * USING TIMESTAMP - * WHERE KEY = keyname; + * WHERE KEY = keyname + [IF (EXISTS | name = value, ...)]; */ deleteStatement returns [DeleteStatement.Parsed expr] @init { Attributes.Raw attrs = new Attributes.Raw(); List columnDeletions = Collections.emptyList(); + boolean ifExists = false; } : K_DELETE ( dels=deleteSelection { columnDeletions = dels; } )? K_FROM cf=columnFamilyName ( usingClauseDelete[attrs] )? K_WHERE wclause=whereClause - ( K_IF conditions=updateConditions )? + ( K_IF ( K_EXISTS { ifExists = true; } | conditions=updateConditions ))? { return new DeleteStatement.Parsed(cf, attrs, columnDeletions, wclause, - conditions == null ? Collections.>emptyList() : conditions); + conditions == null ? Collections.>emptyList() : conditions, + ifExists); } ; diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java index 675ce7a256..799d8eaffd 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -246,7 +246,7 @@ public class BatchStatement implements CQLStatement, MeasurableForPreparedCache { statement.addUpdatesAndConditions(key, clusteringPrefix, updates, conditions, statementVariables, timestamp); // As soon as we have a ifNotExists, we set columnsWithConditions to null so that everything is in the resultSet - if (statement.hasIfNotExistCondition()) + if (statement.hasIfNotExistCondition() || statement.hasIfExistCondition()) columnsWithConditions = null; else if (columnsWithConditions != null) Iterables.addAll(columnsWithConditions, statement.getColumnsWithConditions()); diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasConditions.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasConditions.java index ee004bcc65..a7ec8d4a1b 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasConditions.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasConditions.java @@ -52,7 +52,21 @@ public class CQL3CasConditions implements CASConditions { RowCondition previous = conditions.put(prefix.build(), new NotExistCondition(prefix, now)); if (previous != null && !(previous instanceof NotExistCondition)) - throw new InvalidRequestException("Cannot mix IF conditions and IF NOT EXISTS for the same row"); + { + // 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"); + } + } + + public void addExist(ColumnNameBuilder prefix) throws InvalidRequestException + { + RowCondition previous = conditions.put(prefix.build(), new ExistCondition(prefix, now)); + // this should be prevented by the parser, but it doesn't hurt to check + if (previous != null && previous instanceof NotExistCondition) + throw new InvalidRequestException("Cannot mix IF EXISTS and IF NOT EXISTS conditions for the same row"); } public void addConditions(ColumnNameBuilder prefix, Collection conds, List variables) throws InvalidRequestException @@ -130,6 +144,26 @@ public class CQL3CasConditions implements CASConditions } } + private static class ExistCondition extends RowCondition + { + private ExistCondition(ColumnNameBuilder rowPrefix, long now) + { + super (rowPrefix, now); + } + + public boolean appliesTo(ColumnFamily current) + { + if (current == null) + return false; + + Iterator iter = current.iterator(new ColumnSlice[]{ new ColumnSlice(rowPrefix.build(), rowPrefix.buildAsEndOfRange())}); + while (iter.hasNext()) + if (iter.next().isLive(now)) + return true; + return false; + } + } + private static class ColumnsConditions extends RowCondition { private final Map conditions = new HashMap<>(); diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java index 6efe1008e5..902add42d2 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -101,9 +101,10 @@ public class DeleteStatement extends ModificationStatement Attributes.Raw attrs, List deletions, List whereClause, - List> conditions) + List> conditions, + boolean ifExists) { - super(name, attrs, conditions, false); + super(name, attrs, conditions, false, ifExists); this.deletions = deletions; this.whereClause = whereClause; } diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 154c01cc01..d96ea9c5df 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -73,6 +73,7 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF private List columnConditions; private List staticConditions; private boolean ifNotExists; + private boolean ifExists; private boolean hasNoClusteringColumns = true; private boolean setsOnlyStaticColumns; @@ -195,7 +196,7 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF public Iterable getColumnsWithConditions() { - if (ifNotExists) + if (ifNotExists || ifExists) return null; return Iterables.concat(columnConditions == null ? Collections.emptyList() : Iterables.transform(columnConditions, getColumnForCondition), @@ -230,6 +231,16 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF return ifNotExists; } + public void setIfExistCondition() + { + ifExists = true; + } + + public boolean hasIfExistCondition() + { + return ifExists; + } + private void addKeyValues(CFDefinition.Name name, Restriction values) throws InvalidRequestException { if (name.kind == CFDefinition.Name.Kind.COLUMN_ALIAS) @@ -489,6 +500,7 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF public boolean hasConditions() { return ifNotExists + || ifExists || (columnConditions != null && !columnConditions.isEmpty()) || (staticConditions != null && !staticConditions.isEmpty()); } @@ -562,6 +574,10 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF // of any static columns and we should use the prefix for the "static part" of the partition. conditions.addNotExist(setsOnlyStaticColumns ? cfm.getStaticColumnNameBuilder() : clusteringPrefix); } + else if (ifExists) + { + conditions.addExist(clusteringPrefix); + } else { if (columnConditions != null) @@ -701,15 +717,17 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF public static abstract class Parsed extends CFStatement { protected final Attributes.Raw attrs; - private final List> conditions; + protected final List> conditions; private final boolean ifNotExists; + private final boolean ifExists; - protected Parsed(CFName name, Attributes.Raw attrs, List> conditions, boolean ifNotExists) + protected Parsed(CFName name, Attributes.Raw attrs, List> conditions, boolean ifNotExists, boolean ifExists) { super(name); this.attrs = attrs; this.conditions = conditions == null ? Collections.>emptyList() : conditions; this.ifNotExists = ifNotExists; + this.ifExists = ifExists; } public ParsedStatement.Prepared prepare() throws InvalidRequestException @@ -733,7 +751,7 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF ModificationStatement stmt = prepareInternal(cfDef, boundNames, preparedAttributes); - if (ifNotExists || !conditions.isEmpty()) + if (ifNotExists || ifExists || !conditions.isEmpty()) { if (stmt.isCounter()) throw new InvalidRequestException("Conditional updates are not supported on counter tables"); @@ -746,8 +764,15 @@ public abstract class ModificationStatement implements CQLStatement, MeasurableF // To have both 'IF NOT EXISTS' and some other conditions doesn't make sense. // So far this is enforced by the parser, but let's assert it for sanity if ever the parse changes. assert conditions.isEmpty(); + assert !ifExists; stmt.setIfNotExistCondition(); } + else if (ifExists) + { + assert conditions.isEmpty(); + assert !ifNotExists; + stmt.setIfExistCondition(); + } else { for (Pair entry : conditions) diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index fc9bb664e6..8453a76981 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -118,7 +118,7 @@ public class UpdateStatement extends ModificationStatement List columnNames, List columnValues, boolean ifNotExists) { - super(name, attrs, null, ifNotExists); + super(name, attrs, null, ifNotExists, false); this.columnNames = columnNames; this.columnValues = columnValues; } @@ -189,7 +189,7 @@ public class UpdateStatement extends ModificationStatement List whereClause, List> conditions) { - super(name, attrs, conditions, false); + super(name, attrs, conditions, false, false); this.updates = updates; this.whereClause = whereClause; }