diff --git a/CHANGES.txt b/CHANGES.txt index 9abfadbfde..a8f7d35c2c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Refactor ColumnCondition (CASSANDRA-19620) * Allow configuring log format for Audit Logs (CASSANDRA-19792) * Support for noboolean rpm (centos7 compatible) packages removed (CASSANDRA-19787) * Allow threads waiting for the metadata log follower to be interrupted (CASSANDRA-19761) diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index ac1d3189d1..d774022e19 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -412,10 +412,14 @@ selectionFunction returns [Selectable.Raw s] ; selectionLiteral returns [Term.Raw value] - : c=constant { $value = c; } - | K_NULL { $value = Constants.NULL_LITERAL; } - | ':' id=noncol_ident { $value = newBindVariables(id); } - | QMARK { $value = newBindVariables(null); } + : c=constant { $value = c; } + | K_NULL { $value = Constants.NULL_LITERAL; } + | m=marker { $value = m; } + ; + +marker returns [Term.Raw value] + : ':' id=noncol_ident { $value = newBindVariables(id); } + | QMARK { $value = newBindVariables(null); } ; selectionFunctionArgs returns [List a] @@ -545,17 +549,16 @@ updateStatement returns [UpdateStatement.ParsedUpdate expr] attrs, operations, wclause.build(), - conditions == null ? Collections.>emptyList() : conditions, + conditions == null ? Collections.emptyList() : conditions, ifExists); } ; -updateConditions returns [List> conditions] - @init { conditions = new ArrayList>(); } - : columnCondition[conditions] ( K_AND columnCondition[conditions] )* +updateConditions returns [List conditions] + @init { conditions = new ArrayList(); } + : c1=columnCondition { $conditions.add(c1);} ( K_AND cn=columnCondition { $conditions.add(cn); })* ; - /** * DELETE name1, name2 * FROM @@ -579,7 +582,7 @@ deleteStatement returns [DeleteStatement.Parsed expr] attrs, columnDeletions, wclause.build(), - conditions == null ? Collections.>emptyList() : conditions, + conditions == null ? Collections.emptyList() : conditions, ifExists); } ; @@ -1624,14 +1627,12 @@ value returns [Term.Raw value] | u=usertypeLiteral { $value = u; } | t=tupleLiteral { $value = t; } | K_NULL { $value = Constants.NULL_LITERAL; } - | ':' id=noncol_ident { $value = newBindVariables(id); } - | QMARK { $value = newBindVariables(null); } + | m=marker { $value = m; } ; intValue returns [Term.Raw value] - : t=INTEGER { $value = Constants.Literal.integer($t.text); } - | ':' id=noncol_ident { $value = newBindVariables(id); } - | QMARK { $value = newBindVariables(null); } + : t=INTEGER { $value = Constants.Literal.integer($t.text); } + | m=marker { $value = m; } ; functionName returns [FunctionName s] @@ -1751,28 +1752,19 @@ udtColumnOperation[List> operations, } ; -columnCondition[List> conditions] +columnCondition returns [ColumnCondition.Raw condition] // 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))); } - ) + : column=cident + ( op=relationType t=term { $condition = ColumnCondition.Raw.simpleCondition(column, op, Terms.Raw.of(t)); } + | op=containsOperator t=term { $condition = ColumnCondition.Raw.simpleCondition(column, op, Terms.Raw.of(t)); } + | K_IN v=singleColumnInValues { $condition = ColumnCondition.Raw.simpleCondition(column, Operator.IN, v); } | '[' element=term ']' - ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionCondition(t, element, op))); } - | K_IN - ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, values))); } - | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, marker))); } - ) + ( op=relationType t=term { $condition = ColumnCondition.Raw.collectionElementCondition(column, element, op, Terms.Raw.of(t)); } + | K_IN v=singleColumnInValues { $condition = ColumnCondition.Raw.collectionElementCondition(column, element, Operator.IN, v); } ) | '.' field=fident - ( op=relationType t=term { conditions.add(Pair.create(key, ColumnCondition.Raw.udtFieldCondition(t, field, op))); } - | K_IN - ( values=singleColumnInValues { conditions.add(Pair.create(key, ColumnCondition.Raw.udtFieldInCondition(field, values))); } - | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.udtFieldInCondition(field, marker))); } - ) + ( op=relationType t=term { $condition = ColumnCondition.Raw.udtFieldCondition(column, field, op, Terms.Raw.of(t)); } + | K_IN v=singleColumnInValues { $condition = ColumnCondition.Raw.udtFieldCondition(column, field, Operator.IN, v); } ) ) ; @@ -1807,45 +1799,24 @@ relationType returns [Operator op] ; relation[WhereClause.Builder clauses] - : name=cident type=relationType t=term { $clauses.add(Relation.singleColumn(name, type, t)); } - | name=cident K_BETWEEN betweenValues=singleColumnBetweenValues - { $clauses.add(Relation.singleColumn($name.id, Operator.BETWEEN, betweenValues)); } - | name=cident K_LIKE t=term { $clauses.add(Relation.singleColumn(name, Operator.LIKE, t)); } - | name=cident K_IS K_NOT K_NULL { $clauses.add(Relation.singleColumn(name, Operator.IS_NOT, Constants.NULL_LITERAL)); } - | K_TOKEN l=tupleOfIdentifiers type=relationType t=term { $clauses.add(Relation.token(l, type, t)); } - | K_TOKEN l=tupleOfIdentifiers K_BETWEEN betweenValues=singleColumnBetweenValues { $clauses.add(Relation.token(l, Operator.BETWEEN, betweenValues)); } - | name=cident K_IN marker=inMarker - { $clauses.add(Relation.singleColumn(name, Operator.IN, marker)); } - | name=cident K_IN inValues=singleColumnInValues - { $clauses.add(Relation.singleColumn($name.id, Operator.IN, inValues)); } - | name=cident rt=containsOperator t=term { $clauses.add(Relation.singleColumn(name, rt, t)); } + : name=cident + ( type=relationType t=term { $clauses.add(Relation.singleColumn(name, type, t)); } + | K_BETWEEN betweenValues=singleColumnBetweenValues { $clauses.add(Relation.singleColumn(name, Operator.BETWEEN, betweenValues)); } + | K_LIKE t=term { $clauses.add(Relation.singleColumn(name, Operator.LIKE, t)); } + | K_IS K_NOT K_NULL { $clauses.add(Relation.singleColumn(name, Operator.IS_NOT, Constants.NULL_LITERAL)); } + | K_IN inValue=singleColumnInValues { $clauses.add(Relation.singleColumn(name, Operator.IN, inValue)); } + | rt=containsOperator t=term { $clauses.add(Relation.singleColumn(name, rt, t)); } + ) + | K_TOKEN l=tupleOfIdentifiers + ( type=relationType t=term { $clauses.add(Relation.token(l, type, t)); } + | K_BETWEEN betweenValues=singleColumnBetweenValues { $clauses.add(Relation.token(l, Operator.BETWEEN, betweenValues)); } + ) | name=cident '[' key=term ']' type=relationType t=term { $clauses.add(Relation.mapElement(name, key, type, t)); } | ids=tupleOfIdentifiers - ( K_IN - ( '(' ')' - { $clauses.add(Relation.multiColumn(ids, Operator.IN, Terms.Raw.of(Collections.emptyList()))); } - | tupleInMarker=inMarker /* (a, b, c) IN ? */ - { $clauses.add(Relation.multiColumn(ids, Operator.IN, tupleInMarker)); } - | literals=tupleOfTupleLiterals /* (a, b, c) IN ((1, 2, 3), (4, 5, 6), ...) */ - { - $clauses.add(Relation.multiColumn(ids, Operator.IN, literals)); - } - | markers=tupleOfMarkersForTuples /* (a, b, c) IN (?, ?, ...) */ - { $clauses.add(Relation.multiColumn(ids, Operator.IN, markers)); } - ) - | type=relationType literal=tupleLiteral /* (a, b, c) > (1, 2, 3) or (a, b, c) > (?, ?, ?) */ - { - $clauses.add(Relation.multiColumn(ids, type, literal)); - } - | type=relationType tupleMarker=markerForTuple /* (a, b, c) >= ? */ - { $clauses.add(Relation.multiColumn(ids, type, tupleMarker)); } - | K_BETWEEN - ( t1=tupleLiteral K_AND t2=tupleLiteral - { $clauses.add(Relation.multiColumn(ids, Operator.BETWEEN, Terms.Raw.of(List.of(t1, t2)))); } - | m1=markerForTuple K_AND m2=markerForTuple - { $clauses.add(Relation.multiColumn(ids, Operator.BETWEEN, Terms.Raw.of(List.of(m1, m2)))); } - ) - ) + ( K_IN inValue=multiColumnInValues { $clauses.add(Relation.multiColumn(ids, Operator.IN, inValue)); } + | type=relationType v=multiColumnValue {$clauses.add(Relation.multiColumn(ids, type, v)); } + | K_BETWEEN t1=multiColumnValue K_AND t2=multiColumnValue { $clauses.add(Relation.multiColumn(ids, Operator.BETWEEN, Terms.Raw.of(List.of(t1, t2)))); } + ) | '(' relation[$clauses] ')' ; @@ -1853,7 +1824,7 @@ containsOperator returns [Operator o] : K_CONTAINS { o = Operator.CONTAINS; } (K_KEY { o = Operator.CONTAINS_KEY; })? ; -inMarker returns [InMarker.Raw marker] +inMarker returns [Terms.Raw marker] : QMARK { $marker = newINBindVariables(null); } | ':' name=noncol_ident { $marker = newINBindVariables(name); } ; @@ -1864,26 +1835,38 @@ tupleOfIdentifiers returns [List ids] ; singleColumnInValues returns [Terms.Raw terms] + : t=terms { $terms = t;} + | m=inMarker { $terms = m;} + ; + +terms returns [Terms.Raw terms] @init { List list = new ArrayList<>(); } @after { $terms = Terms.Raw.of(list); } : '(' ( t1 = term { list.add(t1); } (',' ti=term { list.add(ti); })* )? ')' ; +multiColumnValue returns [Term.Raw term] + : l=tupleLiteral { $term = l; } /* (a, b, c) > (1, 2, 3) or (a, b, c) > (?, ?, ?) */ + | m=marker { $term = m; } /* (a, b, c) >= ? */ + ; + +multiColumnInValues returns [Terms.Raw terms] + : '(' ')' { $terms = Terms.Raw.of();} /* (a, b, c) IN () */ + | m=inMarker { $terms = m; } /* (a, b, c) IN ? */ + | tl=tupleOfTupleLiterals { $terms = tl; } /* (a, b, c) IN ((1, 2, 3), (4, 5, 6), ...) */ + | tm=tupleOfMarkersForTuples { $terms = tm; } /* (a, b, c) IN (?, ?, ...) */ + ; + tupleOfTupleLiterals returns [Terms.Raw literals] @init { List list = new ArrayList<>(); } @after { $literals = Terms.Raw.of(list); } : '(' t1=tupleLiteral { list.add(t1); } (',' ti=tupleLiteral { list.add(ti); })* ')' ; -markerForTuple returns [Marker.Raw marker] - : QMARK { $marker = newBindVariables(null); } - | ':' name=noncol_ident { $marker = newBindVariables(name); } - ; - tupleOfMarkersForTuples returns [Terms.Raw markers] @init { List list = new ArrayList<>(); } @after { $markers = Terms.Raw.of(list); } - : '(' m1=markerForTuple { list.add(m1); } (',' mi=markerForTuple { list.add(mi); })* ')' + : '(' m1=marker { list.add(m1); } (',' mi=marker { list.add(mi); })* ')' ; comparatorType returns [CQL3Type.Raw t] diff --git a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java index 26aeb85795..e4da92218b 100644 --- a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java +++ b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java @@ -18,11 +18,13 @@ package org.apache.cassandra.cql3; import java.nio.ByteBuffer; +import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import org.apache.cassandra.cache.IMeasurableMemory; @@ -186,6 +188,17 @@ public class ColumnIdentifier implements IMeasurableMemory, Comparable toCqlStrings(List identifiers) + { + return Lists.transform(identifiers, ColumnIdentifier::toCQLString); + } + /** * Returns a string representation of the identifier that is safe to use directly in CQL queries. * If necessary, the string will be double-quoted, and any quotes inside the string will be escaped. diff --git a/src/java/org/apache/cassandra/cql3/ColumnsExpression.java b/src/java/org/apache/cassandra/cql3/ColumnsExpression.java index b182d27169..8e52623ffd 100644 --- a/src/java/org/apache/cassandra/cql3/ColumnsExpression.java +++ b/src/java/org/apache/cassandra/cql3/ColumnsExpression.java @@ -24,22 +24,19 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.db.marshal.CollectionType; -import org.apache.cassandra.db.marshal.ListType; import org.apache.cassandra.db.marshal.MapType; import org.apache.cassandra.db.marshal.TupleType; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.cql3.statements.RequestValidations.checkContainsNoDuplicates; import static org.apache.cassandra.cql3.statements.RequestValidations.checkContainsOnly; @@ -71,15 +68,15 @@ public final class ColumnsExpression } @Override - AbstractType type(TableMetadata table, List columns) + AbstractType type(TableMetadata table, List columns, ElementExpression element) { return columns.get(0).type; } @Override - String toCQLString(Stream columns, String mapKey) + String toCQLString(List columns, String element) { - return columns.findFirst().orElseThrow(); + return columns.get(0); } @Override @@ -112,15 +109,17 @@ public final class ColumnsExpression } @Override - AbstractType type(TableMetadata table, List columns) + AbstractType type(TableMetadata table, List columns, ElementExpression element) { return new TupleType(ColumnMetadata.typesOf(columns)); } @Override - String toCQLString(Stream columns, String mapKey) + String toCQLString(List columns, String element) { - return columns.collect(Collectors.joining(", ", "(", ")")); + StringBuilder builder = new StringBuilder().append('('); + Joiner.on(", ").appendTo(builder, columns); + return builder.append(')').toString(); } @Override @@ -142,7 +141,7 @@ public final class ColumnsExpression // If the columns do not match the partition key columns, let's try to narrow down the problem checkTrue(new HashSet<>(columns).containsAll(table.partitionKeyColumns()), - "The token() function must be applied to all partition key components or none of them"); + "The token() function must be applied to all partition key components or none of them"); checkContainsNoDuplicates(columns, "The token() function contains duplicate partition key components"); @@ -153,15 +152,18 @@ public final class ColumnsExpression } @Override - AbstractType type(TableMetadata table, List columns) + AbstractType type(TableMetadata table, List columns, ElementExpression element) { return table.partitioner.getTokenValidator(); } @Override - String toCQLString(Stream columns, String mapKey) + String toCQLString(List columns, String element) { - return columns.collect(Collectors.joining(", ", "token(", ")")); + StringBuilder builder = new StringBuilder(); + builder.append("token("); + Joiner.on(", ").appendTo(builder, columns); + return builder.append(')').toString(); } @Override @@ -171,39 +173,26 @@ public final class ColumnsExpression } }, /** - * Map element expression (e.g. {@code columnA[?]}) + * Element expression (e.g. {@code columnA[?]}). This is used for collection elements and UDT fields. For more + * information see {@link ElementExpression}. */ - MAP_ELEMENT + ELEMENT { @Override void validateColumns(TableMetadata table, List columns) { - ColumnMetadata column = columns.get(0); - checkFalse(column.type instanceof ListType, "Indexes on list entries (%s[index] = value) are not supported.", column.name); - checkTrue(column.type instanceof MapType, "Column %s cannot be used as a map", column.name); - checkTrue(column.type.isMultiCell(), "Map-entry predicates on frozen map column %s are not supported", column.name); } @Override - AbstractType type(TableMetadata table, List columns) + AbstractType type(TableMetadata table, List columns, ElementExpression element) { - return ((MapType) columns.get(0).type).getValuesType(); + return element.type(); } @Override - String toCQLString(Stream columns, String mapKey) + String toCQLString(List columns, String element) { - return new StringBuilder().append(columns.findFirst().orElseThrow()) - .append('[') - .append(mapKey) - .append(']') - .toString(); - } - - @Override - public String toString() - { - return "Map element"; + return columns.get(0) + element; } }; @@ -216,49 +205,45 @@ public final class ColumnsExpression /** * Returns the expression type. - * @param table the table metadata - * @param columns the expression columns + * + * @param table the table metadata + * @param columns the expression columns + * @param element the element expression in case of ELEMENT columns expression * @return the expression type */ - abstract AbstractType type(TableMetadata table, List columns); + abstract AbstractType type(TableMetadata table, List columns, ElementExpression element); /** * Returns CQL representation of the expression. - * @param columns the expression's columns - * @param mapKey the key used to access the map element + * + * @param columns the expression's columns + * @param element the element in case of ELEMENT columns expression * @return the CQL representation of the expression. */ - abstract String toCQLString(Stream columns, String mapKey); + abstract String toCQLString(List columns, String element); - String toCQLString(List columns, Term mapKey) + String toCQLString(List columns, ElementExpression elementExpression) { - String k = null; - if (this == Kind.MAP_ELEMENT) - { - CQL3Type type = ((MapType) columns.get(0).type).getKeysType().asCQL3Type(); - // If a Term is not terminal it can be a row marker or a function. - // We ignore the fact that it could be a function for now. - k = mapKey.isTerminal() ? type.toCQLLiteral(((Term.Terminal) mapKey).get()) : "?"; - } - - return toCQLString(columns.stream().map(c -> c.name.toCQLString()), k); + return toCQLString(ColumnMetadata.cqlNames(columns), elementExpression != null ? elementExpression.toCQLString() : ""); } - String toCQLString(List identifiers, Term.Raw rawMapKey) + String toCQLString(List identifiers, ElementExpression.Raw rawElement) { - String mapKey = rawMapKey == null ? null : rawMapKey.getText(); - return toCQLString(identifiers.stream().map(ColumnIdentifier::toCQLString), mapKey); + String element = rawElement == null ? "" : rawElement.toCQLString(); + return toCQLString(ColumnIdentifier.toCqlStrings(identifiers), element); } } - /** - * The kind of columns expression. - */ private final Kind kind; /** - * The type represented by this expression. For example, for a single column the type of the expression will - * be the one of the column and for a map element expression the type will be the one of the map value. + * The type represented by this expression. + *
  • + *
      for a single column the type of the expression will be the one of the column
    + *
      for a multi-column expression the type will be a tuple type
    + *
      for a token expression the type will be the token type
    + *
      for an element expression, the type will be one of the elements of interest(UDT field or collection element)
    + *
  • */ private final AbstractType type; @@ -268,18 +253,27 @@ public final class ColumnsExpression private final List columns; /** - * The key used to access the map element if this expression is for a map element, - * {@code null} otherwise. + * The element if this is an ELEMENT expression, {@code null} otherwise. + * Like UDT field or collection element. */ - private final Term mapKey; + private final ElementExpression element; //Only relevant for ELEMENT kind - - private ColumnsExpression(Kind kind, AbstractType type, List columns, Term mapKey) + ColumnsExpression(Kind kind, AbstractType type, List columns, ElementExpression element) { + assert kind != Kind.ELEMENT || element != null: "Element expression must have an element"; this.kind = kind; this.type = type; this.columns = columns; - this.mapKey = mapKey; + this.element = element; // This could be null for kinds that don't use it + } + + /** + * Returns the expression type. + * @return the expression type. + */ + public AbstractType type() + { + return type; } /** @@ -297,12 +291,11 @@ public final class ColumnsExpression * @param columns the columns * @return an expression for multi-columns. */ + @VisibleForTesting public static ColumnsExpression multiColumns(List columns) { - AbstractType type = new TupleType(columns.stream() - .map(c -> c.type) - .collect(Collectors.toList())); - return new ColumnsExpression(Kind.MULTI_COLUMN, type, ImmutableList.copyOf(columns), null); + AbstractType type = new TupleType(ColumnMetadata.types(columns)); + return new ColumnsExpression(Kind.MULTI_COLUMN, type, ImmutableList.copyOf(columns),null); } /** @@ -338,7 +331,7 @@ public final class ColumnsExpression */ public ColumnMetadata.Kind columnsKind() { - // All columns must have the same type. + // All columns must have the same kind. return firstColumn().kind; } @@ -351,27 +344,44 @@ public final class ColumnsExpression return kind; } - public ByteBuffer mapKey(QueryOptions options) + /** + * Returns the key, index or fieldname specifying the selected element. + * @return the key, index or fieldname specifying the selected element. + */ + public ByteBuffer element(QueryOptions options) { - ByteBuffer key = mapKey.bindAndGet(options); - if (key == null) - throw invalidRequest("Invalid null map key for column %s", firstColumn().name.toCQLString()); - if (key == ByteBufferUtil.UNSET_BYTE_BUFFER) - throw invalidRequest("Invalid unset map key for column %s", firstColumn().name.toCQLString()); - return key; + return element.bindAndGet(options); } /** - * Collects the column specifications for the bind variables in the map key. - * This is obviously a no-op if the expression is not a {@code MAP_ELEMENT} expression. + * Checks if this instance is a collection element expression. + * @return {@code true} if this instance is a collection element expression, {@code false} otherwise. + */ + public boolean isCollectionElementExpression() + { + return kind == Kind.ELEMENT && element != null && element.kind() == ElementExpression.Kind.COLLECTION_ELEMENT; + } + + /** + * Checks if this instance is a map element expression. + * @return {@code true} if this instance is a map element expression, {@code false} otherwise. + */ + public boolean isMapElementExpression() + { + return kind == Kind.ELEMENT && element != null && element.kind() == ElementExpression.Kind.COLLECTION_ELEMENT && firstColumn().type instanceof MapType; + } + + /** + * Collects the column specifications for the bind variables. + * This is obviously a no-op if the expression is not a {@code ELEMENET_EXPRESSION} expression. * * @param boundNames the variables specification where to collect the - * bind variables of the map key in. + * bind variables of the map key/collection element in. */ public void collectMarkerSpecification(VariableSpecifications boundNames) { - if (mapKey != null) - mapKey.collectMarkerSpecification(boundNames); + if (element != null) + element.collectMarkerSpecification(boundNames); } /** @@ -390,8 +400,8 @@ public final class ColumnsExpression */ public void addFunctionsTo(List functions) { - if (mapKey != null) - mapKey.addFunctionsTo(functions); + if (element != null) + element.addFunctionsTo(functions); } /** @@ -400,7 +410,7 @@ public final class ColumnsExpression */ public String toCQLString() { - return kind.toCQLString(columns, mapKey); + return kind.toCQLString(columns, element); } @Override @@ -424,8 +434,8 @@ public final class ColumnsExpression } /** - * The parsed version of the {@code ColumnsExpression} as outputed by the CQL parser. - * {@code Raw.prepare} will be called upon schema binding to create the {@code ColumnsExpression}. + * The parsed version of the {@link ColumnsExpression} as outputed by the CQL parser. + * {@code Raw.prepare} will be called upon schema binding to create the {@link ColumnsExpression}. */ public static final class Raw { @@ -436,13 +446,13 @@ public final class ColumnsExpression */ private final List identifiers; - private final Term.Raw rawMapKey; + private final ElementExpression.Raw rawElement; - private Raw(Kind kind, List identifiers, Term.Raw mapKey) + private Raw(Kind kind, List identifiers, ElementExpression.Raw rawElement) { this.kind = kind; this.identifiers = identifiers; - this.rawMapKey = mapKey; + this.rawElement = rawElement; } /** @@ -466,6 +476,7 @@ public final class ColumnsExpression /** * Creates a raw expression for multi-column (e.g. {@code (columnA, columnB)}). + * * @param identifiers the columns identifier * @return a raw expression for multi-column. */ @@ -476,6 +487,7 @@ public final class ColumnsExpression /** * Creates a raw expression for token restrictions (e.g. {@code token(columnA, columnB)}). + * * @param identifiers the columns identifiers * @return a raw token expression. */ @@ -485,18 +497,32 @@ public final class ColumnsExpression } /** - * Creates a raw expression for a map element restrictions (e.g. {@code columnA[?]}). - * @param identifier the map column identifier - * @param rawMapKey the raw map key + * Creates a raw expression for collection element conditions (e.g. {@code columnA[?]}). + * + * @param identifier the collection element column identifier + * @param rawCollectionElement the raw collection element * @return a raw element expression. */ - public static Raw mapElement(ColumnIdentifier identifier, Term.Raw rawMapKey) + public static Raw collectionElement(ColumnIdentifier identifier, Term.Raw rawCollectionElement) { - return new Raw(Kind.MAP_ELEMENT, ImmutableList.of(identifier), rawMapKey); + return new Raw(Kind.ELEMENT, ImmutableList.of(identifier), new ElementExpression.Raw(rawCollectionElement, null, ElementExpression.Kind.COLLECTION_ELEMENT)); + } + + /** + * Creates a raw expression for a UDT field conditions. + * + * @param identifier the UDT field column identifier + * @param rawUdtField the raw UDT field + * @return a raw element expression. + */ + public static Raw udtField(ColumnIdentifier identifier, FieldIdentifier rawUdtField) + { + return new Raw(Kind.ELEMENT, ImmutableList.of(identifier), new ElementExpression.Raw(null, rawUdtField, ElementExpression.Kind.UDT_FIELD)); } /** * Renames an identifier in this expression, if applicable. + * * @param from the old identifier * @param to the new identifier * @return this object, if the old identifier is not in the set of identifiers that this expression covers; otherwise @@ -510,37 +536,43 @@ public final class ColumnsExpression List newIdentifiers = identifiers.stream() .map(e -> e.equals(from) ? to : e) .collect(Collectors.toList()); - return new Raw(kind, newIdentifiers, rawMapKey); + return new Raw(kind, newIdentifiers, rawElement); } /** - * Bind this {@code Raw} instance to the schema and return the resulting {@code ColumnsExpression}. + * Checks if this raw expression contains bind markers. + * @return {@code true} if this raw expression contains bind markers, {@code false} otherwise. + */ + public boolean containsBindMarkers() + { + return rawElement != null && rawElement.containsBindMarkers(); + } + + /** + * Bind this {@link Raw} instance to the schema and return the resulting {@link ColumnsExpression}. * * @param table the table schema - * @return the {@code ColumnsExpression} resulting from the schema binding + * @return the {@link ColumnsExpression} resulting from the schema binding */ public ColumnsExpression prepare(TableMetadata table) { List columns = getColumnsMetadata(table, identifiers); kind.validateColumns(table, columns); - Term mapKey = prepareMapKey(table, columns); - AbstractType type = kind.type(table, columns); - return new ColumnsExpression(kind, type, columns, mapKey); + + ElementExpression elementExpression = null; + if (kind == Kind.ELEMENT) + elementExpression = rawElement.prepare(columns.get(0)); + + AbstractType type = kind.type(table, columns, elementExpression); + + return new ColumnsExpression(kind, type, columns, elementExpression); } - private Term prepareMapKey(TableMetadata table, List columns) - { - if (kind != Kind.MAP_ELEMENT) - return null; - - ColumnSpecification receiver = CollectionType.Kind.MAP.makeCollectionReceiver(columns.get(0), true); - return rawMapKey.prepare(table.keyspace, receiver); - } - /** * Returns the columns corresponding to the identifiers. * * @param table the table metadata + * @param identifiers the columns identifiers * @return the definition of the columns to which apply the token restriction. * @throws InvalidRequestException if the entity cannot be resolved */ @@ -552,10 +584,20 @@ public final class ColumnsExpression return columns; } + /** + * Returns the columns' identifiers. + * + * @return identifiers. + */ + public List identifiers() + { + return identifiers; + } + @Override public int hashCode() { - return Objects.hash(kind, identifiers, rawMapKey); + return Objects.hash(kind, identifiers, rawElement); } @Override @@ -568,16 +610,17 @@ public final class ColumnsExpression return false; Raw r = (Raw) o; - return kind == r.kind && Objects.equals(identifiers, r.identifiers) && Objects.equals(rawMapKey, r.rawMapKey); + return kind == r.kind && Objects.equals(identifiers, r.identifiers) && Objects.equals(rawElement, r.rawElement); } /** * Returns CQL representation of this raw expression. + * * @return the CQL representation of this raw expression. */ public String toCQLString() { - return kind.toCQLString(identifiers, rawMapKey); + return kind.toCQLString(identifiers, rawElement); } } } diff --git a/src/java/org/apache/cassandra/cql3/ElementExpression.java b/src/java/org/apache/cassandra/cql3/ElementExpression.java new file mode 100644 index 0000000000..6a218fe7db --- /dev/null +++ b/src/java/org/apache/cassandra/cql3/ElementExpression.java @@ -0,0 +1,299 @@ +/* + * 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.cql3; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; + +import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.cql3.terms.Constants; +import org.apache.cassandra.cql3.terms.Lists; +import org.apache.cassandra.cql3.terms.Maps; +import org.apache.cassandra.cql3.terms.Term; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.schema.ColumnMetadata; + +import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +/** + * An element expression representation in case of element column expression. + * + *

    This class can be modified to add support for more element expressions like range of elements, for example.

    + */ + +public final class ElementExpression +{ + /** + * Represent the expression kind + */ + public enum Kind + { + /** + * UDT field expression (e.g. {@code columnA.fieldA}) + */ + UDT_FIELD + { + @Override + public String toCQLString(String element) + { + return '.' + element; + } + + @Override + public String toString() + { + return "UDT field"; + } + }, + /** + * Collection element expression (e.g. {@code columnA[?]}) + */ + COLLECTION_ELEMENT + { + @Override + public String toCQLString(String element) + { + return '[' + element + ']'; + } + + @Override + public String toString() + { + return "collection element"; + } + }; + + public abstract String toCQLString(String element); + } + + /** + * The kind of element expression - udt field or collection element. + */ + private final ElementExpression.Kind kind; + + /** + * The type of the key, index or udt field. + */ + private final AbstractType keyOrIndexType; + + /** + * The term representing the key, index or udt field. + */ + private final Term keyOrIndex; + + /** + * The element type. + */ + private final AbstractType type; + + private ElementExpression(ElementExpression.Kind kind, AbstractType type, AbstractType keyOrIndexType, Term keyOrIndex) + { + this.kind = kind; + this.type = type; + this.keyOrIndexType = keyOrIndexType; + this.keyOrIndex = keyOrIndex; + } + + /** + * Returns the expression kind. + * @return the expression kind. + */ + public ElementExpression.Kind kind() + { + return kind; + } + + /** + * Returns the element type. + * @return the element type. + */ + public AbstractType type() + { + return type; + } + + /** + * Collects the column specifications for the bind variables. + * + * @param boundNames the variables specification where to collect the + * bind variables of the map key/collection element in. + */ + public void collectMarkerSpecification(VariableSpecifications boundNames) + { + keyOrIndex.collectMarkerSpecification(boundNames); + } + + /** + * Adds all functions (native and user-defined) used by any component of the restriction + * to the specified list. + * @param functions the list to add to + */ + public void addFunctionsTo(List functions) + { + keyOrIndex.addFunctionsTo(functions); + } + + /** + * Returns the ByteBuffer representation of the key or index. + * + * @param options the query options + * @return the ByteBuffer representation of the key or index. + */ + public ByteBuffer bindAndGet(QueryOptions options) + { + return keyOrIndex.bindAndGet(options); + } + + public String toCQLString() + { + // If a Term is not terminal it can be a row marker or a function. + // We ignore the fact that it could be a function for now. + + String value = keyOrIndex.isTerminal() ? keyOrIndexType.asCQL3Type().toCQLLiteral(((Term.Terminal) keyOrIndex).get()) : "?"; + return kind.toCQLString(value); + } + + @Override + public String toString() + { + return this.kind.toString(); + } + + public static final class Raw + { + private final Kind kind; + private final Term.Raw rawCollectionElement; + + private final FieldIdentifier udtField; + + Raw(Term.Raw collectionElement, FieldIdentifier udtField, Kind kind) + { + this.rawCollectionElement = collectionElement; + this.udtField = udtField; + this.kind = kind; + } + + /** + * Returns the expression kind. + * @return the expression kind. + */ + public Kind kind() + { + return kind; + } + + /** + * Bind this {@link Raw} instance to the schema and return the resulting {@link ElementExpression}. + * + * @param column the column + * @return the {@link ElementExpression} resulting from the schema binding + */ + ElementExpression prepare(ColumnMetadata column) + { + if (kind == Kind.COLLECTION_ELEMENT) + { + if (!(column.type.isCollection())) + throw invalidRequest("Invalid element access syntax for non-collection column %s", column.name); + + Term term = prepareCollectionElement(column); + CollectionType collectionType = (CollectionType) column.type; + AbstractType elementType = collectionType.valueComparator(); + AbstractType keyOrIndexType = collectionType.isMap() ? ((MapType) collectionType).getKeysType() : Int32Type.instance; + return new ElementExpression(kind, elementType, keyOrIndexType, term); + } + + UserType userType = (UserType) column.type; + int fieldPosition = userType.fieldPosition(udtField); + if (fieldPosition == -1) + throw invalidRequest("Unknown field %s for column %s", udtField, column.name); + + return new ElementExpression(kind, + userType.type(fieldPosition), + UTF8Type.instance, + new Constants.Value(udtField.bytes)); + } + + private Term prepareCollectionElement(ColumnMetadata receiver) + { + ColumnSpecification elementSpec; + switch ((((CollectionType) receiver.type).kind)) + { + case LIST: + elementSpec = Lists.indexSpecOf(receiver); + break; + case MAP: + elementSpec = Maps.keySpecOf(receiver); + break; + case SET: + throw invalidRequest("Invalid element access syntax for set column %s", receiver.name); + default: + throw new AssertionError(); + } + + return rawCollectionElement.prepare(receiver.ksName, elementSpec); + } + + + /** + * Checks if this raw expression contains bind markers. + * @return {@code true} if this raw expression contains bind markers, {@code false} otherwise. + */ + public boolean containsBindMarkers() + { + return rawCollectionElement != null && rawCollectionElement.containsBindMarker(); + } + + @Override + public int hashCode() + { + return Objects.hash(kind, rawCollectionElement, udtField); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (!(o instanceof ElementExpression.Raw)) + return false; + + ElementExpression.Raw r = (ElementExpression.Raw) o; + return kind == r.kind && Objects.equals(rawCollectionElement, r.rawCollectionElement) && Objects.equals(udtField, r.udtField); + } + + public String toCQLString() + { + String element = rawCollectionElement == null ? udtField.toString() : rawCollectionElement.getText(); + return kind.toCQLString(element); + } + + @Override + public String toString() + { + return this.kind.toString(); + } + } +} diff --git a/src/java/org/apache/cassandra/cql3/Operator.java b/src/java/org/apache/cassandra/cql3/Operator.java index e180a9981b..3b86f83fc9 100644 --- a/src/java/org/apache/cassandra/cql3/Operator.java +++ b/src/java/org/apache/cassandra/cql3/Operator.java @@ -23,17 +23,20 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; import com.google.common.collect.RangeSet; import org.apache.cassandra.cql3.restrictions.ClusteringElements; +import org.apache.cassandra.cql3.terms.Terms; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.CollectionType; import org.apache.cassandra.db.marshal.ListType; import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.MultiElementType; import org.apache.cassandra.db.marshal.SetType; -import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.schema.ColumnMetadata; @@ -41,6 +44,7 @@ import org.apache.cassandra.serializers.ListSerializer; import org.apache.cassandra.utils.ByteBufferUtil; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; public enum Operator @@ -56,9 +60,28 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { + // Legacy behavior of LWT conditions + if (leftOperand == null || rightOperand == null) + return leftOperand == rightOperand; + return type.compareForCQL(leftOperand, rightOperand) == 0; } + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + // Legacy behavior of LWT conditions + if (rightOperand == null) + return leftOperand == null; + + List elements = type.unpack(rightOperand); + + if (elements.isEmpty()) + return leftOperand == null; + + return leftOperand != null && type.compareCQL(leftOperand, elements) == 0; + } + @Override public boolean requiresFilteringOrIndexingFor(ColumnMetadata.Kind columnKind) { @@ -81,7 +104,7 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { return true; } @@ -97,7 +120,15 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return type.compareForCQL(leftOperand, rightOperand) < 0; + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + return leftOperand != null && type.compareForCQL(leftOperand, rightOperand) < 0; + } + + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + List elements = unpackMultiCellElements(type, rightOperand); + return leftOperand != null && type.compareCQL(leftOperand, elements) < 0; } @Override @@ -126,9 +157,10 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { - return kind != ColumnsExpression.Kind.MAP_ELEMENT; + // this method is used only in restrictions, not in conditions where different rules apply for now + return expression.kind() != ColumnsExpression.Kind.ELEMENT; } }, LTE(3) @@ -142,7 +174,15 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return type.compareForCQL(leftOperand, rightOperand) <= 0; + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + return leftOperand != null && type.compareForCQL(leftOperand, rightOperand) <= 0; + } + + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + List elements = unpackMultiCellElements(type, rightOperand); + return leftOperand != null && type.compareCQL(leftOperand, elements) <= 0; } @Override @@ -171,10 +211,11 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { - return kind != ColumnsExpression.Kind.MAP_ELEMENT; + return expression.kind() != ColumnsExpression.Kind.ELEMENT; } + }, GTE(1) { @@ -187,7 +228,15 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return type.compareForCQL(leftOperand, rightOperand) >= 0; + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + return leftOperand != null && type.compareForCQL(leftOperand, rightOperand) >= 0; + } + + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + List elements = unpackMultiCellElements(type, rightOperand); + return leftOperand != null && type.compareCQL(leftOperand, elements) >= 0; } @Override @@ -216,9 +265,9 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { - return kind != ColumnsExpression.Kind.MAP_ELEMENT; + return expression.kind() != ColumnsExpression.Kind.ELEMENT; } }, GT(2) @@ -232,7 +281,15 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return type.compareForCQL(leftOperand, rightOperand) > 0; + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + return leftOperand != null && type.compareForCQL(leftOperand, rightOperand) > 0; + } + + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + List elements = unpackMultiCellElements(type, rightOperand); + return leftOperand != null && type.compareCQL(leftOperand, elements) > 0; } @Override @@ -261,9 +318,9 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { - return kind != ColumnsExpression.Kind.MAP_ELEMENT; + return expression.kind() != ColumnsExpression.Kind.ELEMENT; } }, IN(7) @@ -275,8 +332,25 @@ public enum Operator public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); ListSerializer serializer = ListType.getInstance(type, false).getSerializer(); - return serializer.anyMatch(rightOperand, r -> type.compareForCQL(leftOperand, r) == 0); + + if (leftOperand == null) + return serializer.anyMatch(rightOperand, Objects::isNull); + + return serializer.anyMatch(rightOperand, r -> r != null && type.compareForCQL(leftOperand, r) == 0); + } + + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + ListSerializer serializer = ListType.getInstance(type, false).getSerializer(); + + if (leftOperand == null) + return serializer.anyMatch(rightOperand, Objects::isNull); + + return serializer.anyMatch(rightOperand, r -> r != null && type.compareCQL(leftOperand, type.unpack(r)) == 0); } @Override @@ -286,9 +360,9 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { - return kind == ColumnsExpression.Kind.SINGLE_COLUMN || kind == ColumnsExpression.Kind.MULTI_COLUMN; + return expression.kind() == ColumnsExpression.Kind.SINGLE_COLUMN || expression.kind() == ColumnsExpression.Kind.MULTI_COLUMN; } }, CONTAINS(5) @@ -296,6 +370,11 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + + if (leftOperand == null) + return false; + switch(((CollectionType) type).kind) { case LIST: @@ -312,22 +391,10 @@ public enum Operator } @Override - public boolean isSatisfiedBy(CollectionType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) { - for (Cell cell : leftOperand) - { - if (type.kind == CollectionType.Kind.SET) - { - if (type.nameComparator().compare(cell.path().get(0), rightOperand) == 0) - return true; - } - else - { - if (type.valueComparator().compare(cell.buffer(), rightOperand) == 0) - return true; - } - } - return false; + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + return leftOperand != null && ((CollectionType) type).contains(leftOperand, rightOperand); } @Override @@ -353,14 +420,16 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); MapType mapType = (MapType) type; - return mapType.compose(leftOperand).containsKey(mapType.getKeysType().compose(rightOperand)); + return leftOperand != null && mapType.compose(leftOperand).containsKey(mapType.getKeysType().compose(rightOperand)); } @Override - public boolean isSatisfiedBy(CollectionType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) { - return leftOperand.getCell(CellPath.create(rightOperand)) != null; + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", this); + return leftOperand != null && leftOperand.getCell(CellPath.create(rightOperand)) != null; } @Override @@ -386,9 +455,30 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { + // Legacy behavior of LWT conditions + if (leftOperand == null || rightOperand == null) + { + return leftOperand != rightOperand; + } + return type.compareForCQL(leftOperand, rightOperand) != 0; } + @Override + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + { + // Legacy behavior of LWT conditions + if (rightOperand == null) + return leftOperand != null; + + List elements = type.unpack(rightOperand); + + if (elements.isEmpty()) + return leftOperand != null; + + return leftOperand == null || type.compareCQL(leftOperand, elements) != 0; + } + @Override public boolean requiresFilteringOrIndexingFor(ColumnMetadata.Kind columnKind) { @@ -438,7 +528,8 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return ByteBufferUtil.startsWith(leftOperand, rightOperand); + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", LIKE); + return leftOperand != null && ByteBufferUtil.startsWith(leftOperand, rightOperand); } }, LIKE_SUFFIX(11) @@ -452,7 +543,8 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return ByteBufferUtil.endsWith(leftOperand, rightOperand); + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", LIKE); + return leftOperand != null && ByteBufferUtil.endsWith(leftOperand, rightOperand); } }, LIKE_CONTAINS(12) @@ -466,7 +558,8 @@ public enum Operator @Override public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return ByteBufferUtil.contains(leftOperand, rightOperand); + checkTrue(rightOperand != null, "Invalid comparison with null for operator \"%s\"", LIKE); + return leftOperand != null && ByteBufferUtil.contains(leftOperand, rightOperand); } }, LIKE_MATCHES(13) @@ -479,7 +572,7 @@ public enum Operator public boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand) { - return ByteBufferUtil.contains(leftOperand, rightOperand); + return leftOperand != null && ByteBufferUtil.contains(leftOperand, rightOperand); } }, LIKE(14) @@ -556,9 +649,9 @@ public enum Operator } @Override - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { - return kind != ColumnsExpression.Kind.MAP_ELEMENT; + return expression.kind() != ColumnsExpression.Kind.ELEMENT; } }; @@ -642,11 +735,28 @@ public enum Operator public abstract boolean isSatisfiedBy(AbstractType type, ByteBuffer leftOperand, ByteBuffer rightOperand); - public boolean isSatisfiedBy(CollectionType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) + public boolean isSatisfiedBy(MultiElementType type, ComplexColumnData leftOperand, ByteBuffer rightOperand) { throw new UnsupportedOperationException(); } + /** + * Unpack multi-cell elements checking for null value and empty collections + * + * @param type the {@code MultiElementType} + * @param value the value to unpack + * @return the multi-cell elements + * @throws org.apache.cassandra.exceptions.InvalidRequestException if the value is null or an empty collection + */ + List unpackMultiCellElements(MultiElementType type, ByteBuffer value) + { + checkTrue(value != null, "Invalid comparison with null for operator \"%s\"", this); + List elements = type.unpack(value); + if (type.isCollection() && elements.isEmpty()) + throw invalidRequest("Invalid comparison with an empty %s for operator \"%s\"", ((CollectionType) type).kind, this); + return elements; + } + public static int serializedSize() { return 4; @@ -654,7 +764,8 @@ public enum Operator public void validateFor(ColumnsExpression expression) { - if (!canBeUsedWith(expression.kind())) + // this method is used only in restrictions, not in conditions where different rules apply for now + if (!isSupportedByRestrictionsOn(expression)) throw invalidRequest("%s cannot be used with %s relations", this, expression); switch (expression.kind()) @@ -678,7 +789,8 @@ public enum Operator checkFalse(appliesToCollectionElements() && !columnType.isCollection(), "Cannot use %s on non-collection column %s", this, firstColumn.name); } - case MAP_ELEMENT: + // intentional fallthrough - missing break statement + case ELEMENT: ColumnMetadata column = expression.firstColumn(); AbstractType type = column.type; if (type.isMultiCell()) @@ -691,27 +803,27 @@ public enum Operator // We don't support relations against entire collections (unless they're frozen), like "numbers = {1, 2, 3}" checkFalse(type.isCollection() - && !this.appliesToMapKeys() - && !this.appliesToCollectionElements() - && expression.kind() != ColumnsExpression.Kind.MAP_ELEMENT, + && !this.appliesToMapKeys() + && !this.appliesToCollectionElements() + && !expression.isCollectionElementExpression(), "Collection column '%s' (%s) cannot be restricted by a '%s' relation", column.name, type.asCQL3Type(), this); } - break; + break; } } /** - * Checks if the specified expression kind can be used with this operator. - * @param kind the expression kind - * @return {@code true} if the specified expression kind can be used with this operator, {@code false} otherwise. + * Checks if the specified expression kind can be used with this operator in relation. + * @param expression the column expression + * @return {@code true} if the specified expression kind can be used with this operator in a relation, {@code false} otherwise. */ - public boolean canBeUsedWith(ColumnsExpression.Kind kind) + public boolean isSupportedByRestrictionsOn(ColumnsExpression expression) { // All operators support single columns - return kind == ColumnsExpression.Kind.SINGLE_COLUMN; + return expression.kind() == ColumnsExpression.Kind.SINGLE_COLUMN; } /** @@ -838,4 +950,38 @@ public enum Operator .filter(o -> o.isSupportedByReadPath() && !o.isLikeVariant() && o.requiresFilteringOrIndexingFor(columnKind)) .collect(Collectors.toList()); } + + /** + * Builds the CQL String representing the operation between the 2 specified operands. + * + * @param leftOperand the left operand + * @param rightOperand the right operand + * @return the CQL String representing the operation between the 2 specified operands. + */ + public String buildCQLString(ColumnsExpression leftOperand, Terms rightOperand) + { + return buildCQLString(leftOperand.toCQLString(), rightOperand, Terms::asList); + } + + /** + * Builds the CQL String representing the operation between the 2 specified operands. + * + * @param leftOperand the left operand + * @param rightOperand the right operand + * @return the CQL String representing the operation between the 2 specified operands. + */ + public String buildCQLString(ColumnsExpression.Raw leftOperand, Terms.Raw rightOperand) + { + return buildCQLString(leftOperand.toCQLString(), rightOperand, Terms.Raw::asList); + } + + private String buildCQLString(String leftOperand, T rightOperand, Function> asList) + { + if (isTernary()) + { + List terms = asList.apply(rightOperand); + return String.format("%s %s %s AND %s", leftOperand, this, terms.get(0), terms.get(1)); + } + return String.format("%s %s %s", leftOperand, this, rightOperand); + } } diff --git a/src/java/org/apache/cassandra/cql3/Relation.java b/src/java/org/apache/cassandra/cql3/Relation.java index 40350ab3bb..be9112c817 100644 --- a/src/java/org/apache/cassandra/cql3/Relation.java +++ b/src/java/org/apache/cassandra/cql3/Relation.java @@ -20,14 +20,20 @@ package org.apache.cassandra.cql3; import java.util.List; import java.util.Objects; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.cql3.restrictions.SimpleRestriction; import org.apache.cassandra.cql3.restrictions.SingleRestriction; -import org.apache.cassandra.cql3.terms.Term; -import org.apache.cassandra.cql3.terms.Terms; +import org.apache.cassandra.cql3.terms.*; import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.exceptions.InvalidRequestException; -import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest; + +import static org.apache.cassandra.cql3.statements.RequestValidations.*; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; /** * The parsed version of a {@code SimpleRestriction} as outputed by the CQL parser. @@ -94,15 +100,16 @@ public final class Relation * Creates a relation for a map element (e.g. {@code columnA[?] = ?}). * * @param identifier the map column identifier - * @param rawKey the map element key + * @param rawKey the map element key (we do not support list elements in relations yet) * @param operator the relation operator * @param rawTerm the term to which the map element must be compared * @return a relation for a map element. */ - public static Relation mapElement(ColumnIdentifier identifier, Term.Raw rawKey, Operator operator, Term.Raw rawTerm) + @VisibleForTesting + static Relation mapElement(ColumnIdentifier identifier, Term.Raw rawKey, Operator operator, Term.Raw rawTerm) { assert operator.kind() == Operator.Kind.BINARY; - return new Relation(ColumnsExpression.Raw.mapElement(identifier, rawKey), operator, Terms.Raw.of(rawTerm)); + return new Relation(ColumnsExpression.Raw.collectionElement(identifier, rawKey), operator, Terms.Raw.of(rawTerm)); } /** @@ -184,12 +191,22 @@ public final class Relation if (operator == Operator.NEQ) throw invalidRequest("Unsupported '!=' relation: %s", this); - ColumnsExpression expression = rawExpressions.prepare(table); - expression.collectMarkerSpecification(boundNames); + ColumnsExpression columnsExpression = rawExpressions.prepare(table); - operator.validateFor(expression); + // TODO support restrictions on list elements as we do in conditions, then we can probably move below validations + // to ElementExpression prepare/validateColumns + if (columnsExpression.isMapElementExpression()) + { + ColumnMetadata column = columnsExpression.firstColumn(); + checkFalse(column.type instanceof ListType, "Indexes on list entries (%s[index] = value) are not supported.", column.name); + checkTrue(column.type instanceof MapType, "Column %s cannot be used as a map", column.name); + checkTrue(column.type.isMultiCell(), "Map-entry predicates on frozen map column %s are not supported", column.name); + columnsExpression.collectMarkerSpecification(boundNames); + } - ColumnSpecification receiver = expression.columnSpecification(); + operator.validateFor(columnsExpression); + + ColumnSpecification receiver = columnsExpression.columnSpecification(); if (!operator.appliesToColumnValues()) receiver = ((CollectionType) receiver.type).makeCollectionReceiver(receiver, operator.appliesToMapKeys()); @@ -198,9 +215,14 @@ public final class Relation // An IN restriction with only one element is the same as an EQ restriction if (operator.isIN() && terms.containsSingleTerm()) - return new SimpleRestriction(expression, Operator.EQ, terms); + return new SimpleRestriction(columnsExpression, Operator.EQ, terms); - return new SimpleRestriction(expression, operator, terms); + return new SimpleRestriction(columnsExpression, operator, terms); + } + + public ColumnIdentifier column() + { + return rawExpressions.identifiers().get(0); } /** @@ -243,12 +265,7 @@ public final class Relation */ public String toCQLString() { - if (operator.isTernary()) - { - List terms = rawTerms.asList(); - return String.format("%s %s %s AND %s", rawExpressions.toCQLString(), operator, terms.get(0), terms.get(1)); - } - return String.format("%s %s %s", rawExpressions.toCQLString(), operator, rawTerms.getText()); + return operator.buildCQLString(rawExpressions, rawTerms); } @Override diff --git a/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java b/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java index 542ed38e31..3c0aa7ca19 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java +++ b/src/java/org/apache/cassandra/cql3/conditions/ColumnCondition.java @@ -20,41 +20,59 @@ package org.apache.cassandra.cql3.conditions; import java.nio.ByteBuffer; import java.util.*; -import com.google.common.collect.Iterators; +import com.google.common.annotations.VisibleForTesting; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.ColumnsExpression; +import org.apache.cassandra.cql3.FieldIdentifier; +import org.apache.cassandra.cql3.Operator; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.VariableSpecifications; import org.apache.cassandra.cql3.functions.Function; -import org.apache.cassandra.cql3.terms.Constants; -import org.apache.cassandra.cql3.terms.Lists; -import org.apache.cassandra.cql3.terms.Maps; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.cql3.terms.Terms; -import org.apache.cassandra.cql3.terms.UserTypes; -import org.apache.cassandra.db.rows.*; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.CollectionType; +import org.apache.cassandra.db.marshal.CounterColumnType; +import org.apache.cassandra.db.marshal.ListType; +import org.apache.cassandra.db.marshal.MapType; +import org.apache.cassandra.db.marshal.MultiElementType; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; +import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import static org.apache.cassandra.cql3.statements.RequestValidations.*; /** * A CQL3 condition on the value of a column or collection element. For example, "UPDATE .. IF a = 0". */ -public abstract class ColumnCondition +public final class ColumnCondition { - public final ColumnMetadata column; - public final Operator operator; - private final Terms terms; + /** + * The columns expression to which the condition applies. + */ + public final ColumnsExpression columnsExpression; - private ColumnCondition(ColumnMetadata column, Operator op, Terms terms) + /** + * The operator + */ + public final Operator operator; + + /** + * The values + */ + private final Terms values; + + public ColumnCondition(ColumnsExpression columnsExpression, Operator operator, Terms values) { - this.column = column; - this.operator = op; - this.terms = terms; + this.columnsExpression = columnsExpression; + this.operator = operator; + this.values = values; } /** @@ -64,7 +82,8 @@ public abstract class ColumnCondition */ public void addFunctionsTo(List functions) { - terms.addFunctionsTo(functions); + columnsExpression.addFunctionsTo(functions); + values.addFunctionsTo(functions); } /** @@ -75,27 +94,63 @@ public abstract class ColumnCondition */ public void collectMarkerSpecification(VariableSpecifications boundNames) { - terms.collectMarkerSpecification(boundNames); + columnsExpression.collectMarkerSpecification(boundNames); + values.collectMarkerSpecification(boundNames); } - public abstract ColumnCondition.Bound bind(QueryOptions options); - - protected final List bindAndGetTerms(QueryOptions options) + public ColumnCondition.Bound bind(QueryOptions options) { - List buffers = terms.bindAndGet(options); + switch (columnsExpression.kind()) + { + case SINGLE_COLUMN: + return bindSingleColumn(options); + case ELEMENT: + return bindElement(options); + default: + throw new UnsupportedOperationException(); + } + } + + private Bound bindSingleColumn(QueryOptions options) + { + ColumnMetadata column = columnsExpression.firstColumn(); + if (column.type.isMultiCell()) + return new MultiCellBound(column, operator, toValue(column.type, bindAndGetTerms(options))); + + return new SimpleBound(column, operator, toValue(column.type, bindAndGetTerms(options))); + } + + private ColumnCondition.Bound bindElement(QueryOptions options) + { + ColumnMetadata column = columnsExpression.firstColumn(); + ByteBuffer keyOrIndex = columnsExpression.element(options); + if (column.type.isCollection()) + { + checkNotNull(keyOrIndex, "Invalid null value for %s element access", column.type instanceof MapType ? "map" : "list"); + } + return new ElementOrFieldAccessBound(column, keyOrIndex, operator, toValue(columnsExpression.type(), bindAndGetTerms(options))); + } + + private ByteBuffer toValue(AbstractType type, List values) + { + if (operator.isIN()) + return ListType.getInstance(type, false).pack(values); + + ByteBuffer value = values.get(0); + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw invalidRequest("Invalid 'unset' value in condition"); + + return value; + } + + private List bindAndGetTerms(QueryOptions options) + { + List buffers = values.bindAndGet(options); checkFalse(buffers == null && operator.isIN(), "Invalid null list in IN condition"); checkFalse(buffers == Term.UNSET_LIST, "Invalid 'unset' value in condition"); return filterUnsetValuesIfNeeded(buffers, ByteBufferUtil.UNSET_BYTE_BUFFER); } - protected final Terms.Terminals bindTerms(QueryOptions options) - { - Terms.Terminals terminals = terms.bind(options); - checkFalse(terminals == null, "Invalid null list in IN condition"); - checkFalse(terminals == Terms.UNSET_TERMINALS, "Invalid 'unset' value in condition"); - return Terms.Terminals.of(filterUnsetValuesIfNeeded(terminals.asList(), Constants.UNSET_VALUE)); - } - private List filterUnsetValuesIfNeeded(List values, T unsetValue) { if (!operator.isIN()) @@ -111,195 +166,28 @@ public abstract class ColumnCondition return filtered; } - /** - * Simple condition (e.g.
    IF v = 1
    ). - */ - private static final class SimpleColumnCondition extends ColumnCondition + public String toCQLString() { - public SimpleColumnCondition(ColumnMetadata column, Operator op, Terms values) - { - super(column, op, values); - } - - public Bound bind(QueryOptions options) - { - if (column.type.isCollection() && column.type.isMultiCell()) - return new MultiCellCollectionBound(column, operator, bindTerms(options)); - - if (column.type.isUDT() && column.type.isMultiCell()) - return new MultiCellUdtBound(column, operator, bindAndGetTerms(options), options.getProtocolVersion()); - - return new SimpleBound(column, operator, bindAndGetTerms(options)); - } - } - - /** - * A condition on a collection element (e.g.
    IF l[1] = 1
    ). - */ - private static class CollectionElementCondition extends ColumnCondition - { - private final Term collectionElement; - - public CollectionElementCondition(ColumnMetadata column, Term collectionElement, Operator op, Terms values) - { - super(column, op, values); - this.collectionElement = collectionElement; - } - - public void addFunctionsTo(List functions) - { - collectionElement.addFunctionsTo(functions); - super.addFunctionsTo(functions); - } - - public void collectMarkerSpecification(VariableSpecifications boundNames) - { - collectionElement.collectMarkerSpecification(boundNames); - super.collectMarkerSpecification(boundNames); - } - - public Bound bind(QueryOptions options) - { - return new ElementAccessBound(column, collectionElement.bindAndGet(options), operator, bindAndGetTerms(options)); - } - } - - /** - * A condition on a UDT field (e.g.
    IF v.a = 1
    ). - */ - private final static class UDTFieldCondition extends ColumnCondition - { - private final FieldIdentifier udtField; - - public UDTFieldCondition(ColumnMetadata column, FieldIdentifier udtField, Operator op, Terms values) - { - super(column, op, values); - assert udtField != null; - this.udtField = udtField; - } - - public Bound bind(QueryOptions options) - { - return new UDTFieldAccessBound(column, udtField, operator, bindAndGetTerms(options)); - } - } - - /** - * A regular column, simple condition. - */ - public static ColumnCondition condition(ColumnMetadata column, Operator op, Terms terms) - { - return new SimpleColumnCondition(column, op, terms); - } - - /** - * A collection column, simple condition. - */ - public static ColumnCondition condition(ColumnMetadata column, Term collectionElement, Operator op, Terms terms) - { - return new CollectionElementCondition(column, collectionElement, op, terms); - } - - /** - * A UDT column, simple condition. - */ - public static ColumnCondition condition(ColumnMetadata column, FieldIdentifier udtField, Operator op, Terms terms) - { - return new UDTFieldCondition(column, udtField, op, terms); + return operator.buildCQLString(columnsExpression, values); } public static abstract class Bound { - public final ColumnMetadata column; - public final Operator comparisonOperator; + protected final ColumnMetadata column; + protected final Operator operator; + protected final ByteBuffer value; - protected Bound(ColumnMetadata column, Operator operator) + protected Bound(ColumnMetadata column, Operator operator, ByteBuffer value) { this.column = column; - // If the operator is an IN we want to compare the value using an EQ. - this.comparisonOperator = operator.isIN() ? Operator.EQ : operator; + this.operator = operator; + this.value = value; } /** * Validates whether this condition applies to {@code current}. */ public abstract boolean appliesTo(Row row); - - public ByteBuffer getCollectionElementValue() - { - return null; - } - - /** Returns true if the operator is satisfied (i.e. "otherValue operator value == true"), false otherwise. */ - protected static boolean compareWithOperator(Operator operator, AbstractType type, ByteBuffer value, ByteBuffer otherValue) - { - if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) - throw invalidRequest("Invalid 'unset' value in condition"); - - if (value == null) - { - switch (operator) - { - case EQ: - return otherValue == null; - case NEQ: - return otherValue != null; - default: - throw invalidRequest("Invalid comparison with null for operator \"%s\"", operator); - } - } - else if (otherValue == null) - { - // the condition value is not null, so only NEQ can return true - return operator == Operator.NEQ; - } - return operator.isSatisfiedBy(type, otherValue, value); - } - } - - protected static Cell getCell(Row row, ColumnMetadata column) - { - // If we're asking for a given cell, and we didn't got any row from our read, it's - // the same as not having said cell. - return row == null ? null : row.getCell(column); - } - - protected static Cell getCell(Row row, ColumnMetadata column, CellPath path) - { - // If we're asking for a given cell, and we didn't got any row from our read, it's - // the same as not having said cell. - return row == null ? null : row.getCell(column, path); - } - - protected static Iterator> getCells(Row row, ColumnMetadata column) - { - // If we're asking for a complex cells, and we didn't got any row from our read, it's - // the same as not having any cells for that column. - if (row == null) - return Collections.emptyIterator(); - - ComplexColumnData complexData = row.getComplexColumnData(column); - return complexData == null ? Collections.>emptyIterator() : complexData.iterator(); - } - - protected static boolean evaluateComparisonWithOperator(int comparison, Operator operator) - { - // called when comparison != 0 - switch (operator) - { - case EQ: - return false; - case LT: - case LTE: - return comparison < 0; - case GT: - case GTE: - return comparison > 0; - case NEQ: - return true; - default: - throw new AssertionError(); - } } /** @@ -307,534 +195,147 @@ public abstract class ColumnCondition */ private static final class SimpleBound extends Bound { - /** - * The condition values - */ - private final List values; - - private SimpleBound(ColumnMetadata column, Operator operator, List values) + private SimpleBound(ColumnMetadata column, Operator operator, ByteBuffer value) { - super(column, operator); - this.values = values; + super(column, operator, value); } @Override public boolean appliesTo(Row row) { - return isSatisfiedBy(rowValue(row)); + return operator.isSatisfiedBy(column.type, rowValue(row), value); } private ByteBuffer rowValue(Row row) { - Cell c = getCell(row, column); + // If we're asking for a given cell, and we didn't get any row from our read, it's + // the same as not having said cell. + if (row == null) + return null; + + Cell c = row.getCell(column); return c == null ? null : c.buffer(); } - - private boolean isSatisfiedBy(ByteBuffer rowValue) - { - for (ByteBuffer value : values) - { - if (compareWithOperator(comparisonOperator, column.type, value, rowValue)) - return true; - } - return false; - } } /** - * A condition on an element of a collection column. + * A condition on a collection element or a UDT field. */ - private static final class ElementAccessBound extends Bound + private static final class ElementOrFieldAccessBound extends Bound { /** - * The collection element + * The collection element or UDT field type. */ - private final ByteBuffer collectionElement; + private final AbstractType elementType; /** - * The conditions values. + * The map key, list index or UDT fieldname. */ - private final List values; + private final ByteBuffer keyOrIndex; - private ElementAccessBound(ColumnMetadata column, - ByteBuffer collectionElement, - Operator operator, - List values) + + private ElementOrFieldAccessBound(ColumnMetadata column, + ByteBuffer keyOrIndex, + Operator operator, + ByteBuffer value) { - super(column, operator); - - this.collectionElement = collectionElement; - this.values = values; + super(column, operator, value); + this.elementType = ((MultiElementType) column.type).elementType(keyOrIndex); + this.keyOrIndex = keyOrIndex; } @Override public boolean appliesTo(Row row) { - boolean isMap = column.type instanceof MapType; - - if (collectionElement == null) - throw invalidRequest("Invalid null value for %s element access", isMap ? "map" : "list"); - - if (isMap) - { - MapType mapType = (MapType) column.type; - ByteBuffer rowValue = rowMapValue(mapType, row); - return isSatisfiedBy(mapType.getKeysType(), rowValue); - } - - ListType listType = (ListType) column.type; - ByteBuffer rowValue = rowListValue(listType, row); - return isSatisfiedBy(listType.getElementsType(), rowValue); + ByteBuffer element = ((MultiElementType) column.type).getElement(columnData(row), keyOrIndex); + return operator.isSatisfiedBy(elementType, element, value); } - private ByteBuffer rowMapValue(MapType type, Row row) + /** + * Returns the column data for the given row. + * @param row the row + * @return the column data for the given row. + */ + private ColumnData columnData(Row row) { - if (column.type.isMultiCell()) - { - Cell cell = getCell(row, column, CellPath.create(collectionElement)); - return cell == null ? null : cell.buffer(); - } - - Cell cell = getCell(row, column); - return cell == null - ? null - : type.getSerializer().getSerializedValue(cell.buffer(), collectionElement, type.getKeysType()); - } - - private ByteBuffer rowListValue(ListType type, Row row) - { - if (column.type.isMultiCell()) - return cellValueAtIndex(getCells(row, column), getListIndex(collectionElement)); - - Cell cell = getCell(row, column); - return cell == null - ? null - : type.getSerializer().getElement(cell.buffer(), getListIndex(collectionElement)); - } - - private static ByteBuffer cellValueAtIndex(Iterator> iter, int index) - { - int adv = Iterators.advance(iter, index); - if (adv == index && iter.hasNext()) - return iter.next().buffer(); - - return null; - } - - private boolean isSatisfiedBy(AbstractType valueType, ByteBuffer rowValue) - { - for (ByteBuffer value : values) - { - if (compareWithOperator(comparisonOperator, valueType, value, rowValue)) - return true; - } - return false; - } - - @Override - public ByteBuffer getCollectionElementValue() - { - return collectionElement; - } - - private static int getListIndex(ByteBuffer collectionElement) - { - int idx = ByteBufferUtil.toInt(collectionElement); - checkFalse(idx < 0, "Invalid negative list index %d", idx); - return idx; + return row == null ? null : row.getColumnData(column); } } /** - * A condition on an entire collection column. + * A condition on a multicell column. */ - private static final class MultiCellCollectionBound extends Bound + private static final class MultiCellBound extends Bound { - private final Terms.Terminals values; - - public MultiCellCollectionBound(ColumnMetadata column, Operator operator, Terms.Terminals values) + public MultiCellBound(ColumnMetadata column, Operator operator, ByteBuffer value) { - super(column, operator); + super(column, operator, value); assert column.type.isMultiCell(); - this.values = values; } - @Override public boolean appliesTo(Row row) { - CollectionType type = (CollectionType) column.type; - - // copy iterator contents so that we can properly reuse them for each comparison with an IN value - for (Term.Terminal value : values.asList()) - { - Iterator> iter = getCells(row, column); - if (value == null || (comparisonOperator.appliesToColumnValues() && isEmpty(value))) - { - if (comparisonOperator == Operator.EQ) - { - if (!iter.hasNext()) - return true; - continue; - } - - if (comparisonOperator == Operator.NEQ) - return iter.hasNext(); - - if (value == null) - throw invalidRequest("Invalid comparison with null for operator \"%s\"", comparisonOperator); - - throw invalidRequest("Invalid comparison with an empty %s for operator \"%s\"", type.kind, comparisonOperator); - } - - if (valueAppliesTo(type, iter, value, comparisonOperator)) - return true; - } - return false; - } - - private boolean isEmpty(Term.Terminal value) - { - return value.getElements().isEmpty(); - } - - private static boolean valueAppliesTo(CollectionType type, Iterator> iter, Term.Terminal value, Operator operator) - { - if (!iter.hasNext() && operator != Operator.NEQ) - return false; - - if(operator == Operator.CONTAINS || operator == Operator.CONTAINS_KEY) - return containsAppliesTo(type, iter, value.get(), operator); - - switch (type.kind) - { - case LIST: - return listAppliesTo((ListType)type, iter, value.getElements(), operator); - case SET: - return setAppliesTo((SetType)type, iter, value.getElements(), operator); - case MAP: - return mapAppliesTo((MapType)type, iter, value.getElements(), operator); - } - throw new AssertionError(); - } - - private static boolean setOrListAppliesTo(AbstractType type, Iterator> iter, Iterator conditionIter, Operator operator, boolean isSet) - { - while(iter.hasNext()) - { - if (!conditionIter.hasNext()) - return (operator == Operator.GT) || (operator == Operator.GTE) || (operator == Operator.NEQ); - - // for lists we use the cell value; for sets we use the cell name - ByteBuffer cellValue = isSet ? iter.next().path().get(0) : iter.next().buffer(); - int comparison = type.compare(cellValue, conditionIter.next()); - if (comparison != 0) - return evaluateComparisonWithOperator(comparison, operator); - } - - if (conditionIter.hasNext()) - return (operator == Operator.LT) || (operator == Operator.LTE) || (operator == Operator.NEQ); - - // they're equal - return operator == Operator.EQ || operator == Operator.LTE || operator == Operator.GTE; - } - - private static boolean listAppliesTo(ListType type, Iterator> iter, List elements, Operator operator) - { - return setOrListAppliesTo(type.getElementsType(), iter, elements.iterator(), operator, false); - } - - private static boolean setAppliesTo(SetType type, Iterator> iter, List elements, Operator operator) - { - // The elements are alread sorted as expected by the SetType - return setOrListAppliesTo(type.getElementsType(), iter, elements.iterator(), operator, true); - } - - private static boolean mapAppliesTo(MapType type, Iterator> iter, List elements, Operator operator) - { - Iterator conditionIter = elements.iterator(); - while(iter.hasNext()) - { - if (!conditionIter.hasNext()) - return (operator == Operator.GT) || (operator == Operator.GTE) || (operator == Operator.NEQ); - - ByteBuffer key = conditionIter.next(); - ByteBuffer value = conditionIter.next(); - Cell c = iter.next(); - - // compare the keys - int comparison = type.getKeysType().compare(c.path().get(0), key); - if (comparison != 0) - return evaluateComparisonWithOperator(comparison, operator); - - // compare the values - comparison = type.getValuesType().compare(c.buffer(), value); - if (comparison != 0) - return evaluateComparisonWithOperator(comparison, operator); - } - - if (conditionIter.hasNext()) - return (operator == Operator.LT) || (operator == Operator.LTE) || (operator == Operator.NEQ); - - // they're equal - return operator == Operator.EQ || operator == Operator.LTE || operator == Operator.GTE; - } - } - - 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 == Operator.CONTAINS_KEY ? ((MapType)type).getKeysType() : ((MapType)type).getValuesType(); - break; - default: - throw new AssertionError(); - } - boolean appliesToSetOrMapKeys = (type.kind == CollectionType.Kind.SET || type.kind == CollectionType.Kind.MAP && operator == Operator.CONTAINS_KEY); - 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 - */ - private static final class UDTFieldAccessBound extends Bound - { - /** - * The UDT field. - */ - private final FieldIdentifier field; - - /** - * The conditions values. - */ - private final List values; - - private UDTFieldAccessBound(ColumnMetadata column, FieldIdentifier field, Operator operator, List values) - { - super(column, operator); - assert column.type.isUDT() && field != null; - this.field = field; - this.values = values; - } - - @Override - public boolean appliesTo(Row row) - { - return isSatisfiedBy(rowValue(row)); - } - - private ByteBuffer rowValue(Row row) - { - UserType userType = (UserType) column.type; - - if (column.type.isMultiCell()) - { - Cell cell = getCell(row, column, userType.cellPathForField(field)); - return cell == null ? null : cell.buffer(); - } - - Cell cell = getCell(row, column); - return cell == null - ? null - : userType.unpack(cell.buffer()).get(userType.fieldPosition(field)); - } - - private boolean isSatisfiedBy(ByteBuffer rowValue) - { - UserType userType = (UserType) column.type; - int fieldPosition = userType.fieldPosition(field); - AbstractType valueType = userType.fieldType(fieldPosition); - for (ByteBuffer value : values) - { - if (compareWithOperator(comparisonOperator, valueType, value, rowValue)) - return true; - } - return false; - } - - @Override - public String toString() - { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); - } - } - - /** - * A condition on an entire UDT. - */ - private static final class MultiCellUdtBound extends Bound - { - /** - * The conditions values. - */ - private final List values; - - /** - * The protocol version - */ - private final ProtocolVersion protocolVersion; - - private MultiCellUdtBound(ColumnMetadata column, Operator op, List values, ProtocolVersion protocolVersion) - { - super(column, op); - assert column.type.isMultiCell(); - this.values = values; - this.protocolVersion = protocolVersion; - } - - @Override - public boolean appliesTo(Row row) - { - return isSatisfiedBy(rowValue(row)); - } - - private final ByteBuffer rowValue(Row row) - { - UserType userType = (UserType) column.type; - Iterator> iter = getCells(row, column); - return iter.hasNext() ? userType.serializeForNativeProtocol(iter, protocolVersion) : null; - } - - private boolean isSatisfiedBy(ByteBuffer rowValue) - { - for (ByteBuffer value : values) - { - if (compareWithOperator(comparisonOperator, column.type, value, rowValue)) - return true; - } - return false; - } - - @Override - public String toString() - { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + ComplexColumnData columnData = row == null ? null : row.getComplexColumnData(column); + return operator.isSatisfiedBy((MultiElementType) column.type, columnData, value); } } public static class Raw { - private final Term.Raw value; - private final Terms.Raw inValues; - - // Can be null, only used with the syntax "IF m[e] = ..." (in which case it's 'e') - private final Term.Raw collectionElement; - - // Can be null, only used with the syntax "IF udt.field = ..." (in which case it's 'field') - private final FieldIdentifier udtField; + private final ColumnsExpression.Raw rawExpressions; private final Operator operator; - private Raw(Term.Raw value, Terms.Raw inValues, Term.Raw collectionElement, - FieldIdentifier udtField, Operator op) + private final Terms.Raw values; + + private Raw(ColumnsExpression.Raw columnExpressions, Operator op, Terms.Raw values) { - this.value = value; - this.inValues = inValues; - this.collectionElement = collectionElement; - this.udtField = udtField; + this.rawExpressions = columnExpressions; this.operator = op; + this.values = values; } - /** A condition on a column. For example: "IF col = 'foo'" */ - public static Raw simpleCondition(Term.Raw value, Operator op) + /** + * Create condition on a column. For example: "IF col = 'foo'" or "IF col IN ('foo', 'bar', ...)" + */ + public static Raw simpleCondition(ColumnIdentifier column, Operator op, Terms.Raw values) { - return new Raw(value, null, null, null, op); + return new Raw(ColumnsExpression.Raw.singleColumn(column), op, values); } - /** An IN condition on a column. For example: "IF col IN ('foo', 'bar', ...)" */ - public static Raw simpleInCondition(Terms.Raw inValues) + /** + * Create a condition on a collection element. For example: "IF col['key'] = 'foo'" + */ + public static Raw collectionElementCondition(ColumnIdentifier column, Term.Raw collectionElement, Operator op, Terms.Raw values) { - return new Raw(null, inValues, null, null, Operator.IN); + return new Raw(ColumnsExpression.Raw.collectionElement(column, collectionElement), op, values); } - /** A condition on a collection element. For example: "IF col['key'] = 'foo'" */ - public static Raw collectionCondition(Term.Raw value, Term.Raw collectionElement, Operator op) + /** + * Create a condition on a UDT field. For example: "IF col.field = 'foo'" + */ + public static Raw udtFieldCondition(ColumnIdentifier column, FieldIdentifier udtField, Operator op, Terms.Raw values) { - return new Raw(value, null, collectionElement, null, op); + return new Raw(ColumnsExpression.Raw.udtField(column, udtField), op, values); } - /** An IN condition on a collection element. For example: "IF col['key'] IN ('foo', 'bar', ...)" */ - public static Raw collectionInCondition(Term.Raw collectionElement, Terms.Raw inValues) + public ColumnsExpression.Raw columnExpression() { - return new Raw(null, inValues, collectionElement, null, Operator.IN); + return rawExpressions; } - /** A condition on a UDT field. For example: "IF col.field = 'foo'" */ - public static Raw udtFieldCondition(Term.Raw value, FieldIdentifier udtField, Operator op) + public ColumnCondition prepare(TableMetadata table) { - return new Raw(value, null, null, udtField, op); - } + ColumnsExpression expression = rawExpressions.prepare(table); + ColumnSpecification receiver = expression.columnSpecification(); - /** An IN condition on a collection element. For example: "IF col.field IN ('foo', 'bar', ...)" */ - public static Raw udtFieldInCondition(FieldIdentifier udtField, Terms.Raw inValues) - { - return new Raw(null, inValues, null, udtField, Operator.IN); - } + checkFalse(expression.columnsKind().isPrimaryKeyKind(), "PRIMARY KEY column '%s' cannot have IF conditions", receiver.name); - public ColumnCondition prepare(String keyspace, ColumnMetadata receiver, TableMetadata cfm) - { if (receiver.type instanceof CounterColumnType) throw invalidRequest("Conditions on counters are not supported"); - if (collectionElement != null) - { - if (!(receiver.type.isCollection())) - throw invalidRequest("Invalid element access syntax for non-collection column %s", receiver.name); - - ColumnSpecification elementSpec, valueSpec; - switch ((((CollectionType) receiver.type).kind)) - { - case LIST: - elementSpec = Lists.indexSpecOf(receiver); - valueSpec = Lists.valueSpecOf(receiver); - break; - case MAP: - elementSpec = Maps.keySpecOf(receiver); - valueSpec = Maps.valueSpecOf(receiver); - break; - case SET: - throw invalidRequest("Invalid element access syntax for set column %s", receiver.name); - default: - throw new AssertionError(); - } - - validateOperationOnDurations(valueSpec.type); - return condition(receiver, collectionElement.prepare(keyspace, elementSpec), operator, prepareTerms(keyspace, valueSpec)); - } - - if (udtField != null) - { - UserType userType = (UserType) receiver.type; - int fieldPosition = userType.fieldPosition(udtField); - if (fieldPosition == -1) - throw invalidRequest("Unknown field %s for column %s", udtField, receiver.name); - - ColumnSpecification fieldReceiver = UserTypes.fieldSpecOf(receiver, fieldPosition); - validateOperationOnDurations(fieldReceiver.type); - return condition(receiver, udtField, operator, prepareTerms(keyspace, fieldReceiver)); - } - validateOperationOnDurations(receiver.type); - return condition(receiver, operator, prepareTerms(keyspace, receiver)); + return new ColumnCondition(expression, operator, prepareTerms(table.keyspace, receiver)); } private Terms prepareTerms(String keyspace, ColumnSpecification receiver) @@ -844,15 +345,10 @@ public abstract class ColumnCondition checkFalse(operator == Operator.CONTAINS && !(receiver.type.isCollection()), "Cannot use CONTAINS on non-collection column %s", receiver.name); - if (operator.isIN()) - { - return inValues.prepare(keyspace, receiver); - } - if (operator == Operator.CONTAINS || operator == Operator.CONTAINS_KEY) receiver = ((CollectionType) receiver.type).makeCollectionReceiver(receiver, operator == Operator.CONTAINS_KEY); - return Terms.of(value.prepare(keyspace, receiver)); + return values.prepare(keyspace, receiver); } private void validateOperationOnDurations(AbstractType type) @@ -866,15 +362,25 @@ public abstract class ColumnCondition } } - public Term.Raw getValue() + /** + * Checks if this raw condition contains bind markers. + * @return {@code true} if this raw condition contains bind markers, {@code false} otherwise. + */ + public boolean containsBindMarkers() { - return value; + return rawExpressions.containsBindMarkers() || values.containsBindMarkers(); + } + + @VisibleForTesting + public String toCQLString() + { + return operator.buildCQLString(rawExpressions, values); } @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + return toCQLString(); } } } diff --git a/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java b/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java index 35d4a9570f..030d4a9719 100644 --- a/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java +++ b/src/java/org/apache/cassandra/cql3/conditions/ColumnConditions.java @@ -73,7 +73,7 @@ public final class ColumnConditions extends AbstractConditions public Collection getColumns() { return Stream.concat(columnConditions.stream(), staticConditions.stream()) - .map(e -> e.column) + .map(e -> e.columnsExpression.firstColumn()) .collect(Collectors.toList()); } @@ -139,7 +139,7 @@ public final class ColumnConditions extends AbstractConditions public Builder add(ColumnCondition condition) { List conds; - if (condition.column.isStatic()) + if (condition.columnsExpression.firstColumn().isStatic()) { if (staticConditions.isEmpty()) staticConditions = new ArrayList<>(); diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java index c45964a997..eb9bc3e8bb 100644 --- a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java +++ b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java @@ -237,5 +237,16 @@ public class FunctionCall extends Term.NonTerminal name.appendCqlTo(cqlNameBuilder); return cqlNameBuilder + terms.stream().map(Term.Raw::getText).collect(Collectors.joining(", ", "(", ")")); } + + @Override + public boolean containsBindMarker() + { + for (Term.Raw t : terms) + { + if (t.containsBindMarker()) + return true; + } + return false; + } } } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java index c58eb70985..223930a957 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/SimpleRestriction.java @@ -142,7 +142,8 @@ public final class SimpleRestriction implements SingleRestriction { return operator == Operator.CONTAINS || operator == Operator.CONTAINS_KEY - || columnsExpression.kind() == ColumnsExpression.Kind.MAP_ELEMENT; + // TODO only map elements supported for now in restrictions + || columnsExpression.isMapElementExpression(); } @Override @@ -150,14 +151,16 @@ public final class SimpleRestriction implements SingleRestriction { // The need for filtering or indexing is a combination of columns expression type and operator // Therefore, we have to take both into account. - return columnsExpression.kind() == ColumnsExpression.Kind.MAP_ELEMENT + // TODO only map elements supported for now in restrictions + return (columnsExpression.isMapElementExpression()) || operator.requiresFilteringOrIndexingFor(columnsExpression.columnsKind()); } @Override public void addFunctionsTo(List functions) { - columnsExpression.addFunctionsTo(functions); + if (columnsExpression.isMapElementExpression()) + columnsExpression.addFunctionsTo(functions); values.addFunctionsTo(functions); } @@ -319,12 +322,11 @@ public final class SimpleRestriction implements SingleRestriction if (isOnToken()) throw new UnsupportedOperationException(); + ColumnMetadata column = firstColumn(); switch (columnsExpression.kind()) { case SINGLE_COLUMN: List buffers = bindAndGet(options); - - ColumnMetadata column = firstColumn(); if (operator == Operator.IN || operator == Operator.BETWEEN) { filter.add(column, operator, multiInputOperatorValues(column, buffers)); @@ -374,10 +376,18 @@ public final class SimpleRestriction implements SingleRestriction } } break; - case MAP_ELEMENT: - ByteBuffer key = columnsExpression.mapKey(options); - List values = bindAndGet(options); - filter.addMapEquality(firstColumn(), key, operator, values.get(0)); + case ELEMENT: + // TODO only map elements supported for now + if (columnsExpression.isMapElementExpression()) + { + ByteBuffer key = columnsExpression.element(options); + if (key == null) + throw invalidRequest("Invalid null map key for column %s", firstColumn().name.toCQLString()); + if (key == ByteBufferUtil.UNSET_BYTE_BUFFER) + throw invalidRequest("Invalid unset map key for column %s", firstColumn().name.toCQLString()); + List values = bindAndGet(options); + filter.addMapEquality(firstColumn(), key, operator, values.get(0)); + } break; default: throw new UnsupportedOperationException(); } @@ -385,18 +395,12 @@ public final class SimpleRestriction implements SingleRestriction private static ByteBuffer multiInputOperatorValues(ColumnMetadata column, List values) { - return ListType.getInstance(column.type, false).pack(values); } @Override public String toString() { - if (operator.isTernary()) - { - List terms = values.asList(); - return String.format("%s %s %s AND %s", columnsExpression.toCQLString(), operator, terms.get(0), terms.get(1)); - } - return String.format("%s %s %s", columnsExpression.toCQLString(), operator, values); + return operator.buildCQLString(columnsExpression, values); } } diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java index 9671592c16..0d322691c6 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java @@ -17,11 +17,8 @@ */ package org.apache.cassandra.cql3.statements; -import java.nio.ByteBuffer; import java.util.*; -import com.google.common.collect.*; - import org.apache.cassandra.db.marshal.TimeUUIDType; import org.apache.cassandra.index.IndexRegistry; import org.apache.cassandra.schema.TableMetadata; @@ -37,7 +34,6 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.service.CASRequest; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.paxos.Ballot; -import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.TimeUUID; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -374,7 +370,7 @@ public class CQL3CasRequest implements CASRequest private static class ColumnsConditions extends RowCondition { - private final Multimap, ColumnCondition.Bound> conditions = HashMultimap.create(); + private final Set conditions = new HashSet<>(); private ColumnsConditions(Clustering clustering) { @@ -385,15 +381,14 @@ public class CQL3CasRequest implements CASRequest { for (ColumnCondition condition : conds) { - ColumnCondition.Bound current = condition.bind(options); - conditions.put(Pair.create(condition.column.name, current.getCollectionElementValue()), current); + conditions.add(condition.bind(options)); } } public boolean appliesTo(FilteredPartition current) throws InvalidRequestException { Row row = current.getRow(clustering); - for (ColumnCondition.Bound condition : conditions.values()) + for (ColumnCondition.Bound condition : conditions) { if (!condition.appliesTo(row)) return false; diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java index 7a50a9b015..0bc2284255 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -33,7 +33,6 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.utils.Pair; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -132,7 +131,7 @@ public class DeleteStatement extends ModificationStatement Attributes.Raw attrs, List deletions, WhereClause whereClause, - List> conditions, + List conditions, boolean ifExists) { super(name, StatementType.DELETE, attrs, conditions, false, ifExists); diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 7696bf378a..9de5dc1666 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -64,7 +64,6 @@ import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.triggers.TriggerExecutor; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.MD5Digest; -import org.apache.cassandra.utils.Pair; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkNull; @@ -890,14 +889,14 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa { protected final StatementType type; private final Attributes.Raw attrs; - private final List> conditions; + private final List conditions; private final boolean ifNotExists; private final boolean ifExists; protected Parsed(QualifiedName name, StatementType type, Attributes.Raw attrs, - List> conditions, + List conditions, boolean ifNotExists, boolean ifExists) { @@ -970,13 +969,11 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa ColumnConditions.Builder builder = ColumnConditions.newBuilder(); - for (Pair entry : conditions) + for (ColumnCondition.Raw rawCondition : conditions) { - ColumnMetadata def = metadata.getExistingColumn(entry.left); - ColumnCondition condition = entry.right.prepare(keyspace(), def, metadata); + ColumnCondition condition = rawCondition.prepare(metadata); condition.collectMarkerSpecification(bindVariables); - checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY column '%s' cannot have IF conditions", def.name); builder.add(condition); } return builder.build(); @@ -991,7 +988,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa /** * Creates the restrictions. * - * @param metadata the column family meta data + * @param metadata the table meta data * @param boundNames the bound names * @param operations the column operations * @param where the where clause @@ -1013,7 +1010,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa return new StatementRestrictions(state, type, metadata, where, boundNames, orderings, applyOnlyToStaticColumns, false, false); } - public List> getConditions() + public List getConditions() { return conditions; } diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index b7d38bc291..378b827a36 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -291,7 +291,7 @@ public class UpdateStatement extends ModificationStatement Attributes.Raw attrs, List> updates, WhereClause whereClause, - List> conditions, + List conditions, boolean ifExists) { super(name, StatementType.UPDATE, attrs, conditions, false, ifExists); diff --git a/src/java/org/apache/cassandra/cql3/terms/InMarker.java b/src/java/org/apache/cassandra/cql3/terms/InMarker.java index 452856e284..8059f741b1 100644 --- a/src/java/org/apache/cassandra/cql3/terms/InMarker.java +++ b/src/java/org/apache/cassandra/cql3/terms/InMarker.java @@ -159,6 +159,12 @@ public final class InMarker extends Terms.NonTerminals return "?"; } + @Override + public boolean containsBindMarkers() + { + return true; + } + private static ColumnSpecification makeInReceiver(ColumnSpecification receiver) { ColumnIdentifier inName = new ColumnIdentifier("in(" + receiver.name + ')', true); diff --git a/src/java/org/apache/cassandra/cql3/terms/Marker.java b/src/java/org/apache/cassandra/cql3/terms/Marker.java index 2befe633ce..48fa02e9f4 100644 --- a/src/java/org/apache/cassandra/cql3/terms/Marker.java +++ b/src/java/org/apache/cassandra/cql3/terms/Marker.java @@ -137,5 +137,11 @@ public final class Marker extends Term.NonTerminal { return "?"; } + + @Override + public boolean containsBindMarker() + { + return true; + } } } diff --git a/src/java/org/apache/cassandra/cql3/terms/MultiElements.java b/src/java/org/apache/cassandra/cql3/terms/MultiElements.java index 7136640d2c..40d4a20e78 100644 --- a/src/java/org/apache/cassandra/cql3/terms/MultiElements.java +++ b/src/java/org/apache/cassandra/cql3/terms/MultiElements.java @@ -83,7 +83,7 @@ public final class MultiElements @Override public ByteBuffer get() { - return type.pack(elements); + return elements == null ? null : type.pack(elements); } @Override diff --git a/src/java/org/apache/cassandra/cql3/terms/Term.java b/src/java/org/apache/cassandra/cql3/terms/Term.java index 36b96a88cd..b7babb5e5e 100644 --- a/src/java/org/apache/cassandra/cql3/terms/Term.java +++ b/src/java/org/apache/cassandra/cql3/terms/Term.java @@ -198,6 +198,15 @@ public interface Term { return this == o || (o instanceof Raw && getText().equals(((Raw) o).getText())); } + + /** + * Checks if this term is or contains bind markers. + * @return {@code true} if this term is or contains bind markers, {@code false} otherwise. + */ + public boolean containsBindMarker() + { + return false; + } } /** diff --git a/src/java/org/apache/cassandra/cql3/terms/Terms.java b/src/java/org/apache/cassandra/cql3/terms/Terms.java index 519f94c505..4a4e6b8442 100644 --- a/src/java/org/apache/cassandra/cql3/terms/Terms.java +++ b/src/java/org/apache/cassandra/cql3/terms/Terms.java @@ -21,6 +21,7 @@ import java.nio.ByteBuffer; import java.util.*; import java.util.stream.Collectors; +import com.google.common.collect.ImmutableList; import org.apache.cassandra.cql3.AssignmentTestable; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.QueryOptions; @@ -141,6 +142,9 @@ public interface Terms */ static Terms of(final List terms) { + if (terms.isEmpty()) + return Terminals.of(); + boolean allTerminals = terms.stream().allMatch(Term::isTerminal); if (allTerminals) @@ -194,6 +198,38 @@ public interface Terms */ abstract class Raw implements AssignmentTestable { + private static final Raw EMPTY = new Raw() + { + @Override + public Terms prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException + { + return Terminals.of(); + } + + @Override + public String getText() + { + return ""; + } + + @Override + public AbstractType getExactTypeIfKnown(String keyspace) + { + return null; + } + + @Override + public TestResult testAssignment(String keyspace, ColumnSpecification receiver) + { + return TestResult.WEAKLY_ASSIGNABLE; + } + + @Override + public boolean containsBindMarkers() + { + return false; + } + }; /** * This method validates this {@code Terms.Raw} is valid for the provided column * specification and "prepare" this {@code Terms.Raw}, returning the resulting {@link Terms}. @@ -254,8 +290,21 @@ public interface Terms return getText(); } + /** + * Checks if these terms are or contains bind markers. + * @return {@code true} if tthese terms are or contains bind markers, {@code false} otherwise. + */ + public abstract boolean containsBindMarkers(); + + public static Raw of() + { + return EMPTY; + } + public static Raw of(List raws) { + if (raws.isEmpty()) + return EMPTY; return new Raw() { @Override @@ -292,6 +341,17 @@ public interface Terms { return AssignmentTestable.TestResult.WEAKLY_ASSIGNABLE; } + + @Override + public boolean containsBindMarkers() + { + for (Term.Raw raw : raws) + { + if (raw.containsBindMarker()) + return true; + } + return false; + } }; } @@ -327,6 +387,12 @@ public interface Terms { return raw.testAssignment(keyspace, receiver); } + + @Override + public boolean containsBindMarkers() + { + return raw.containsBindMarker(); + } }; } } @@ -336,6 +402,41 @@ public interface Terms */ abstract class Terminals implements Terms { + /** + * Empty Terminals. + */ + private static final Terminals EMPTY = new Terminals() + { + @Override + public List get() + { + return ImmutableList.of(); + } + + @Override + public List> getElements() + { + return ImmutableList.of(); + } + + @Override + public List asList() + { + return ImmutableList.of(); + } + + @Override + public void addFunctionsTo(List functions) + { + + } + + @Override + public boolean containsSingleTerm() + { + return false; + } + }; @Override public void collectMarkerSpecification(VariableSpecifications boundNames) {} @@ -375,6 +476,15 @@ public interface Terms */ public abstract List asList(); + /** + * Returns an empty {@code Terminals}. + * @return an empty {@code Terminals}. + */ + public static Terminals of() + { + return EMPTY; + } + /** * Converts a {@code Terminal} into a {@code Terminals}. * @param terminal the {@code Terminal} to convert diff --git a/src/java/org/apache/cassandra/db/marshal/CollectionType.java b/src/java/org/apache/cassandra/db/marshal/CollectionType.java index 483ab1f9c0..b4bcc78d40 100644 --- a/src/java/org/apache/cassandra/db/marshal/CollectionType.java +++ b/src/java/org/apache/cassandra/db/marshal/CollectionType.java @@ -35,6 +35,7 @@ import org.apache.cassandra.cql3.terms.Sets; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.serializers.CollectionSerializer; @@ -424,4 +425,24 @@ public abstract class CollectionType extends MultiElementType } public abstract void forEach(ByteBuffer input, Consumer action); + + public final int compareCQL(ComplexColumnData columnData, List elements) + { + Iterator> cellIterator = columnData.iterator(); + Iterator elementIter = elements.iterator(); + while(cellIterator.hasNext()) + { + if (!elementIter.hasNext()) + return 1; + + int comparison = compareNextCell(cellIterator, elementIter); + if (comparison != 0) + return comparison; + } + return elementIter.hasNext() ? -1 : 0; + } + + protected abstract int compareNextCell(Iterator> cellIterator, Iterator elementIter); + + public abstract boolean contains(ComplexColumnData columnData, ByteBuffer value); } diff --git a/src/java/org/apache/cassandra/db/marshal/ListType.java b/src/java/org/apache/cassandra/db/marshal/ListType.java index f0c7fe214f..94d302d059 100644 --- a/src/java/org/apache/cassandra/db/marshal/ListType.java +++ b/src/java/org/apache/cassandra/db/marshal/ListType.java @@ -25,19 +25,26 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import javax.annotation.Nullable; + import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.ListSerializer; import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.JsonUtils; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.bytecomparable.ByteComparable.Version; import org.apache.cassandra.utils.bytecomparable.ByteSource; +import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; + public class ListType extends CollectionType> { // interning instances @@ -272,4 +279,59 @@ public class ListType extends CollectionType> } return buffers; } + + @Override + protected int compareNextCell(Iterator> cellIterator, Iterator elementIter) + { + return getElementsType().compare(cellIterator.next().buffer(), elementIter.next()); + } + + @Override + public boolean contains(ComplexColumnData columnData, ByteBuffer value) + { + Iterator> iter = columnData.iterator(); + while(iter.hasNext()) + { + ByteBuffer cellValue = iter.next().buffer(); + if(valueComparator().compare(cellValue, value) == 0) + return true; + } + return false; + } + + @Override + public AbstractType elementType(ByteBuffer keyOrIndex) + { + return getElementsType(); + } + + @Override + public ByteBuffer getElement(@Nullable ColumnData columnData, ByteBuffer keyOrIndex) + { + if (columnData == null) + return null; + + int idx = listIndex(keyOrIndex); + + if (isMultiCell()) + { + ComplexColumnData complexColumnData = (ComplexColumnData) columnData; + + if (idx >= complexColumnData.cellsCount()) + return null; + + Cell cell = complexColumnData.getCellByIndex(idx); + return cell == null ? null : cell.buffer(); + } + + List cells = unpack(((Cell) columnData).buffer()); + return idx >= cells.size() ? null : cells.get(idx); + } + + private int listIndex(ByteBuffer index) + { + int idx = ByteBufferUtil.toInt(index); + checkFalse(idx < 0, "Invalid negative list index %d", idx); + return idx; + } } diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java index 72765d6863..69ea6d17e1 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -29,9 +29,14 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import javax.annotation.Nullable; + import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.CollectionSerializer; @@ -404,4 +409,51 @@ public class MapType extends CollectionType> } return sortedBuffers; } + + protected int compareNextCell(Iterator> cellIterator, Iterator elementIter) + { + Cell c = cellIterator.next(); + + // compare the keys + int comparison = getKeysType().compare(c.path().get(0), elementIter.next()); + if (comparison != 0) + return comparison; + + // compare the values + return getValuesType().compare(c.buffer(), elementIter.next()); + } + + @Override + public boolean contains(ComplexColumnData columnData, ByteBuffer value) + { + Iterator> iter = columnData.iterator(); + while(iter.hasNext()) + { + ByteBuffer cellValue = iter.next().buffer(); + if(valueComparator().compare(cellValue, value) == 0) + return true; + } + return false; + } + + @Override + public AbstractType elementType(ByteBuffer keyOrIndex) + { + return getValuesType(); + } + + @Override + public ByteBuffer getElement(@Nullable ColumnData columnData, ByteBuffer keyOrIndex) + { + if (columnData == null) + return null; + + if (isMultiCell()) + { + Cell cell = ((ComplexColumnData) columnData).getCell(CellPath.create(keyOrIndex)); + return cell == null ? null : cell.buffer(); + } + + return getSerializer().getSerializedValue(((Cell) columnData).buffer(), keyOrIndex, getValuesType()); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/MultiElementType.java b/src/java/org/apache/cassandra/db/marshal/MultiElementType.java index ed4f88cc74..d9c229c9e9 100644 --- a/src/java/org/apache/cassandra/db/marshal/MultiElementType.java +++ b/src/java/org/apache/cassandra/db/marshal/MultiElementType.java @@ -21,6 +21,11 @@ package org.apache.cassandra.db.marshal; import java.nio.ByteBuffer; import java.util.List; +import javax.annotation.Nullable; + +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; + /** * Base type for the types being composed of multi-elements like Collections, Tuples, UDTs or Vectors. * This class unifies the methods used by the CQL layer to work with those types, @@ -66,5 +71,37 @@ public abstract class MultiElementType extends AbstractType * @return the elements filtered and sorted as they are used for serialization. */ public abstract List filterSortAndValidateElements(List buffers); + + /** + * Compares the multicell value represensted by the column data with the specified elements. + * @param columnData the column data representing the multicell value + * @param elements the elements to compare + * @return a negative integer, zero, or a positive integer as the column data is less than, equal to, or greater than the elements. + * @throws UnsupportedOperationException if the comparison is not supported by this type. + */ + public abstract int compareCQL(ComplexColumnData columnData, List elements); + + /** + * Returns the type of the element at the specified key or index (optional operation). + * @param keyOrIndex the key or index of the element + * @return the type of the element at the specified key or index. + * @throws UnsupportedOperationException if this method is not supported by this type. + */ + public AbstractType elementType(ByteBuffer keyOrIndex) + { + throw new UnsupportedOperationException(this + " does not support retrieving element types by key or index"); + } + + /** + * Returns the element of the column data at the specified key or index (optional operation). + * @param columnData the column data representing the multicell value + * @param keyOrIndex the key or index of the element to return + * @return the element of the column data at the specified key or index. + * @throws UnsupportedOperationException if this method is not supported by this type. + */ + public ByteBuffer getElement(@Nullable ColumnData columnData, ByteBuffer keyOrIndex) + { + throw new UnsupportedOperationException(this + " does not support retrieving elements by key or index"); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/SetType.java b/src/java/org/apache/cassandra/db/marshal/SetType.java index 39ac241e1b..c2fdf0042c 100644 --- a/src/java/org/apache/cassandra/db/marshal/SetType.java +++ b/src/java/org/apache/cassandra/db/marshal/SetType.java @@ -25,6 +25,8 @@ import java.util.function.Consumer; import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.MarshalException; @@ -257,4 +259,16 @@ public class SetType extends CollectionType> } return new ArrayList<>(sorted); } + + @Override + protected int compareNextCell(Iterator> cellIterator, Iterator elementIter) + { + return getElementsType().compare(cellIterator.next().path().get(0), elementIter.next()); + } + + @Override + public boolean contains(ComplexColumnData columnData, ByteBuffer value) + { + return columnData.getCell(CellPath.create(value)) != null; + } } diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java index 0897431714..feb9b405fc 100644 --- a/src/java/org/apache/cassandra/db/marshal/TupleType.java +++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java @@ -34,6 +34,7 @@ import org.apache.cassandra.cql3.terms.Constants; import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.SyntaxException; @@ -609,4 +610,10 @@ public class TupleType extends MultiElementType return serializer.serialize(pack(buffers)); } + + @Override + public int compareCQL(ComplexColumnData columnData, List fields) + { + throw new UnsupportedOperationException("Multicell tuples are not supported"); + } } diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index 816b019dcf..7addfef63d 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -21,6 +21,8 @@ import java.nio.ByteBuffer; import java.util.*; import java.util.stream.Collectors; +import javax.annotation.Nullable; + import com.google.common.base.Objects; import com.google.common.collect.Lists; @@ -33,6 +35,8 @@ import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.ColumnData; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.schema.Difference; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.TypeSerializer; @@ -428,6 +432,70 @@ public class UserType extends TupleType implements SchemaElement return fieldTypes().stream().anyMatch(f -> f.referencesDuration()); } + @Override + public int compareCQL(ComplexColumnData columnData, List fields) + { + Iterator> cellIter = columnData.iterator(); + int i = 0; + while (cellIter.hasNext()) + { + if (i == fields.size()) + return 1; + + Cell cell = cellIter.next(); + short position = ByteBufferUtil.toShort(cell.path().get(0)); + + while (i < position) + { + if (i == fields.size()) + return 1; + + if (fields.get(i++) != null) + return -1; + } + + ByteBuffer fieldValue = fields.get(i); + + if (fieldValue == null) + return 1; + + int comparison = type(i++).compare(cell.buffer(), fieldValue); + if (comparison != 0) + return comparison; + } + + while(i < fields.size()) + { + if (fields.get(i++) != null) + return -1; + } + + return 0; + } + + @Override + public AbstractType elementType(ByteBuffer keyOrIndex) + { + return type(fieldPosition(new FieldIdentifier(keyOrIndex))); + } + + @Override + public ByteBuffer getElement(@Nullable ColumnData columnData, ByteBuffer keyOrIndex) + { + if (columnData == null) + return null; + + FieldIdentifier field = new FieldIdentifier(keyOrIndex); + + if (isMultiCell()) + { + Cell cell = ((ComplexColumnData) columnData).getCell(cellPathForField(field)); + return cell == null ? null : cell.buffer(); + } + + return unpack(((Cell) columnData).buffer()).get(fieldPosition(field)); + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/db/marshal/VectorType.java b/src/java/org/apache/cassandra/db/marshal/VectorType.java index 455f2425fc..ac4c0bfb94 100644 --- a/src/java/org/apache/cassandra/db/marshal/VectorType.java +++ b/src/java/org/apache/cassandra/db/marshal/VectorType.java @@ -32,6 +32,7 @@ import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.terms.MultiElements; import org.apache.cassandra.cql3.terms.Term; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.TypeSerializer; @@ -695,4 +696,10 @@ public final class VectorType extends MultiElementType> checkConsumedFully(input, accessor, offset); } } + + @Override + public int compareCQL(ComplexColumnData columnData, List elements) + { + throw new UnsupportedOperationException(); + } } diff --git a/src/java/org/apache/cassandra/schema/ColumnMetadata.java b/src/java/org/apache/cassandra/schema/ColumnMetadata.java index 682db5124d..cb3c879a94 100644 --- a/src/java/org/apache/cassandra/schema/ColumnMetadata.java +++ b/src/java/org/apache/cassandra/schema/ColumnMetadata.java @@ -27,6 +27,7 @@ import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.functions.masking.ColumnMask; @@ -405,6 +406,28 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta return Collections2.transform(definitions, columnDef -> columnDef.name); } + /** + * Returns the types corresponding to the specified column definitions. + * + * @param columns the column definitions to convert. + * @return the types corresponding to the specified definitions + */ + public static List> types(List columns) + { + return Lists.transform(columns, column -> column.type); + } + + /** + * Returns the CQL names corresponding to the specified column definitions. + * + * @param columns the column definitions to convert. + * @return the CQL names corresponding to the specified definitions + */ + public static List cqlNames(List columns) + { + return Lists.transform(columns, column -> column.name.toCQLString()); + } + public int compareTo(ColumnMetadata other) { if (this == other) diff --git a/src/java/org/apache/cassandra/serializers/ListSerializer.java b/src/java/org/apache/cassandra/serializers/ListSerializer.java index 2ce3b89af1..020abd2f46 100644 --- a/src/java/org/apache/cassandra/serializers/ListSerializer.java +++ b/src/java/org/apache/cassandra/serializers/ListSerializer.java @@ -150,18 +150,11 @@ public class ListSerializer extends CollectionSerializer> for (int i = 0; i < s; i++) { - int size = accessor.getInt(input, offset); - if (size < 0) - continue; - - offset += TypeSizes.INT_SIZE; - - V value = accessor.slice(input, offset, size); + V value = readValue(input, accessor, offset); + offset += sizeOfValue(value, accessor); if (predicate.test(value)) return true; - - offset += size; } return false; } diff --git a/test/unit/org/apache/cassandra/cql3/RelationTest.java b/test/unit/org/apache/cassandra/cql3/RelationTest.java index 46927e1569..852da6f6d3 100644 --- a/test/unit/org/apache/cassandra/cql3/RelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/RelationTest.java @@ -32,10 +32,10 @@ import org.apache.cassandra.cql3.terms.Terms; import org.apache.cassandra.cql3.terms.Tuples; import static java.util.Arrays.asList; -import static org.apache.cassandra.cql3.Relation.mapElement; -import static org.apache.cassandra.cql3.Relation.multiColumn; -import static org.apache.cassandra.cql3.Relation.singleColumn; -import static org.apache.cassandra.cql3.Relation.token; +import static org.apache.cassandra.cql3.Operator.EQ; +import static org.apache.cassandra.cql3.Relation.*; +import static org.apache.cassandra.cql3.conditions.ColumnCondition.Raw.collectionElementCondition; +import static org.apache.cassandra.cql3.conditions.ColumnCondition.Raw.udtFieldCondition; import static org.junit.Assert.assertEquals; public class RelationTest @@ -76,7 +76,13 @@ public class RelationTest assertEquals("token(col, col2) = ?", token(asList(col, col2), Operator.EQ, marker).toCQLString()); assertEquals("token(col, col2) = token(1, 2)", token(asList(col, col2), Operator.EQ, tokenCall).toCQLString()); - assertEquals("col['text'] = ?", mapElement(col, text, Operator.EQ, marker).toCQLString()); - assertEquals("col[?] = ?", mapElement(col, marker, Operator.EQ, marker).toCQLString()); + assertEquals("col['text'] = ?", mapElement(col, text, EQ, marker).toCQLString()); + assertEquals("col[?] = ?", collectionElementCondition(col, marker, EQ, Terms.Raw.of(marker)).toCQLString()); + + // element access is not allowed for sets + + FieldIdentifier f = FieldIdentifier.forQuoted("f1"); + assertEquals("col.f1 = ?", udtFieldCondition(col, f, EQ, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col.f1 = 1", udtFieldCondition(col, f, EQ, Terms.Raw.of(one)).toCQLString()); } } diff --git a/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java b/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java index c372254c63..771c8c3570 100644 --- a/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/conditions/ColumnConditionTest.java @@ -20,6 +20,7 @@ package org.apache.cassandra.cql3.conditions; import java.nio.ByteBuffer; import java.util.*; +import org.apache.cassandra.cql3.terms.*; import org.junit.Assert; import org.junit.Test; @@ -41,9 +42,21 @@ import org.apache.cassandra.utils.TimeUUID; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import static java.util.Arrays.asList; -import static org.apache.cassandra.cql3.Operator.*; +import static org.apache.cassandra.cql3.Operator.EQ; +import static org.apache.cassandra.cql3.Operator.NEQ; +import static org.apache.cassandra.cql3.Operator.LT; +import static org.apache.cassandra.cql3.Operator.LTE; +import static org.apache.cassandra.cql3.Operator.GT; +import static org.apache.cassandra.cql3.Operator.GTE; +import static org.apache.cassandra.cql3.Operator.CONTAINS; +import static org.apache.cassandra.cql3.Operator.CONTAINS_KEY; +import static org.apache.cassandra.cql3.conditions.ColumnCondition.Raw.simpleCondition; +import static org.apache.cassandra.cql3.conditions.ColumnCondition.Raw.collectionElementCondition; +import static org.apache.cassandra.cql3.conditions.ColumnCondition.Raw.udtFieldCondition; import static org.apache.cassandra.utils.ByteBufferUtil.EMPTY_BYTE_BUFFER; @@ -126,7 +139,9 @@ public class ColumnConditionTest private static boolean appliesSimpleCondition(ByteBuffer rowValue, Operator op, ByteBuffer conditionValue) { ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", Int32Type.instance); - ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(new Constants.Value(conditionValue))); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); + Terms terms = Terms.of(new Constants.Value(conditionValue)); + ColumnCondition condition = new ColumnCondition(column, op, terms); ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -135,8 +150,9 @@ public class ColumnConditionTest { ListType type = ListType.getInstance(Int32Type.instance, true); ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", type); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); Term term = conditionValue == null ? Constants.NULL_VALUE : new MultiElements.Value(type, conditionValue); - ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(term)); + ColumnCondition condition = new ColumnCondition(column, op, Terms.of(term)); ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -144,7 +160,9 @@ public class ColumnConditionTest 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))); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); + Terms terms = Terms.of(new Constants.Value(conditionValue)); + ColumnCondition condition = new ColumnCondition(column, op, terms); ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -152,7 +170,9 @@ public class ColumnConditionTest 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))); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); + Terms terms = Terms.of(new Constants.Value(conditionValue)); + ColumnCondition condition = new ColumnCondition(column, op, terms); ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -160,9 +180,10 @@ public class ColumnConditionTest private static boolean appliesSetCondition(SortedSet rowValue, Operator op, SortedSet conditionValue) { SetType type = SetType.getInstance(Int32Type.instance, true); - ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", SetType.getInstance(Int32Type.instance, true)); + ColumnMetadata definition = ColumnMetadata.regularColumn("ks", "cf", "c", type); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); Term term = conditionValue == null ? Constants.NULL_VALUE : new MultiElements.Value(type, new ArrayList<>(conditionValue)); - ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(term)); + ColumnCondition condition = new ColumnCondition(column, op, Terms.of(term)); ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -170,7 +191,10 @@ public class ColumnConditionTest 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))); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); + Terms terms = Terms.of(new Constants.Value(conditionValue)); + ColumnCondition condition = new ColumnCondition(column, op, terms); + ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -194,7 +218,8 @@ public class ColumnConditionTest } term = new MultiElements.Value(type, value); } - ColumnCondition condition = ColumnCondition.condition(definition, op, Terms.of(term)); + ColumnsExpression column = ColumnsExpression.singleColumn(definition); + ColumnCondition condition = new ColumnCondition(column, op, Terms.of(term)); ColumnCondition.Bound bound = condition.bind(QueryOptions.DEFAULT); return bound.appliesTo(newRow(definition, rowValue)); } @@ -285,7 +310,7 @@ public class ColumnConditionTest private static List list(ByteBuffer... values) { - return Arrays.asList(values); + return asList(values); } @Test @@ -400,7 +425,7 @@ public class ColumnConditionTest private static SortedSet set(ByteBuffer... values) { SortedSet results = new TreeSet<>(Int32Type.instance); - results.addAll(Arrays.asList(values)); + results.addAll(asList(values)); return results; } @@ -671,4 +696,46 @@ public class ColumnConditionTest 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)); } + + @Test + public void toCQLStringTest() + { + ColumnIdentifier col = new ColumnIdentifier("col", false); + Marker.Raw marker = new Marker.Raw(0); + InMarker.Raw inMarker = new InMarker.Raw(0); + Term.Raw one = Constants.Literal.integer("1"); + Term.Raw two = Constants.Literal.integer("2"); + Terms.Raw oneTwo = Terms.Raw.of(asList(one, two)); + Term.Raw text = Constants.Literal.string("text"); + + assertEquals("col = ?", simpleCondition(col, EQ, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col = 2", simpleCondition(col, EQ, Terms.Raw.of(two)).toCQLString()); + assertEquals("col = 'text'", simpleCondition(col, EQ, Terms.Raw.of(text)).toCQLString()); + assertEquals("col >= ?", simpleCondition(col, Operator.GTE, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col IN ?", simpleCondition(col, Operator.IN, inMarker).toCQLString()); + assertEquals("col IN (1, 2)", simpleCondition(col, Operator.IN, oneTwo).toCQLString()); + assertEquals("col IN (1)", simpleCondition(col, Operator.IN, Terms.Raw.of(List.of(one))).toCQLString()); + assertEquals("col BETWEEN 1 AND 2", simpleCondition(col, Operator.BETWEEN, oneTwo).toCQLString()); + + assertEquals("col CONTAINS 1", simpleCondition(col, CONTAINS, Terms.Raw.of(one)).toCQLString()); + assertEquals("col CONTAINS KEY 1", simpleCondition(col, CONTAINS_KEY, Terms.Raw.of(one)).toCQLString()); + assertEquals("col CONTAINS KEY ?", simpleCondition(col, CONTAINS_KEY, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col CONTAINS ?", simpleCondition(col, CONTAINS, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col CONTAINS ?", simpleCondition(col, CONTAINS, Terms.Raw.of(marker)).toCQLString()); + + Term.Raw set = new Sets.Literal(asList(one, two)); + Term.Raw set2 = new Sets.Literal(List.of(Constants.Literal.string("baz"))); + + assertEquals("col = {1, 2}", simpleCondition(col, EQ, Terms.Raw.of(set)).toCQLString()); + assertEquals("col != {'baz'}", simpleCondition(col, Operator.NEQ, Terms.Raw.of(set2)).toCQLString()); + + assertEquals("col['text'] = ?", collectionElementCondition(col, text, EQ, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col[?] = ?", collectionElementCondition(col, marker, EQ, Terms.Raw.of(marker)).toCQLString()); + + // element access is not allowed for sets + + FieldIdentifier f = FieldIdentifier.forQuoted("f1"); + assertEquals("col.f1 = ?", udtFieldCondition(col, f, EQ, Terms.Raw.of(marker)).toCQLString()); + assertEquals("col.f1 = 1", udtFieldCondition(col, f, EQ, Terms.Raw.of(one)).toCQLString()); + } } 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 b14e681e31..a2e4be9b91 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionCollectionsTest.java @@ -1121,6 +1121,10 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester row(true)); // Does not apply + 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 = ?", list("bar")), row(false, null)); assertRows(execute("UPDATE %s SET l = null WHERE k = 0 IF l < ?", list("a")), @@ -1133,8 +1137,9 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester 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 'unset' value in condition", + "UPDATE %s SET l = null WHERE k = 0 IF l CONTAINS ?", unset()); assertInvalidMessage("Invalid comparison with null for operator \"CONTAINS\"", "UPDATE %s SET l = null WHERE k = 0 IF l CONTAINS ?", (ByteBuffer) null); @@ -1165,8 +1170,9 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester 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 'unset' value in condition", + "UPDATE %s SET s = null WHERE k = 0 IF s CONTAINS ?", unset()); assertInvalidMessage("Invalid comparison with null for operator \"CONTAINS\"", "UPDATE %s SET s = null WHERE k = 0 IF s CONTAINS ?", (ByteBuffer) null); @@ -1197,13 +1203,13 @@ public class InsertUpdateIfConditionCollectionsTest extends CQLTester 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 'unset' value in condition", + "UPDATE %s SET m = null WHERE k = 0 IF m CONTAINS ?", unset()); + assertInvalidMessage("Invalid 'unset' value in condition", + "UPDATE %s SET m = null WHERE k = 0 IF m CONTAINS KEY ?", unset()); 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\"", diff --git a/tools/stress/src/org/apache/cassandra/stress/StressProfile.java b/tools/stress/src/org/apache/cassandra/stress/StressProfile.java index 598a4f327a..1460a90a27 100644 --- a/tools/stress/src/org/apache/cassandra/stress/StressProfile.java +++ b/tools/stress/src/org/apache/cassandra/stress/StressProfile.java @@ -40,6 +40,7 @@ import org.antlr.runtime.RecognitionException; import org.apache.cassandra.config.YamlConfigurationLoader; import org.apache.cassandra.cql3.CQLFragmentParser; import org.apache.cassandra.cql3.CqlParser; +import org.apache.cassandra.cql3.conditions.ColumnCondition; import org.apache.cassandra.cql3.statements.ModificationStatement; import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; import org.apache.cassandra.exceptions.RequestValidationException; @@ -416,7 +417,7 @@ public class StressProfile implements Serializable * for dynamic condition we have to read existing db value and then * use current db values during the update. */ - return modificationStatement.getConditions().stream().anyMatch(condition -> condition.right.getValue().getText().equals("?")); + return modificationStatement.getConditions().stream().anyMatch(ColumnCondition.Raw::containsBindMarkers); } public Operation getBulkReadQueries(String name, Timer timer, StressSettings settings, TokenRangeIterator tokenRangeIterator, boolean isWarmup) diff --git a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/CASQuery.java b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/CASQuery.java index 1487a0d3af..1dc44cfc45 100644 --- a/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/CASQuery.java +++ b/tools/stress/src/org/apache/cassandra/stress/operations/userdefined/CASQuery.java @@ -41,7 +41,6 @@ import org.apache.cassandra.stress.generate.values.Generator; import org.apache.cassandra.stress.report.Timer; import org.apache.cassandra.stress.settings.StressSettings; import org.apache.cassandra.stress.util.JavaDriverClient; -import org.apache.cassandra.utils.Pair; import java.io.IOException; import java.util.ArrayList; @@ -81,15 +80,15 @@ public class CASQuery extends SchemaStatement throw new IllegalArgumentException("could not parse update query:" + statement.getQueryString(), e); } - final List> casConditionList = modificationStatement.getConditions(); + final List casConditionList = modificationStatement.getConditions(); List casConditionIndex = new ArrayList<>(); boolean first = true; StringBuilder casReadConditionQuery = new StringBuilder(); casReadConditionQuery.append("SELECT "); - for (final Pair condition : casConditionList) + for (final ColumnCondition.Raw condition : casConditionList) { - if (!condition.right.getValue().getText().equals("?")) + if (!condition.containsBindMarkers()) { //condition uses static value, ignore it continue; @@ -98,8 +97,9 @@ public class CASQuery extends SchemaStatement { casReadConditionQuery.append(", "); } - casReadConditionQuery.append(condition.left.toString()); - casConditionIndex.add(getDataSpecification().partitionGenerator.indexOf(condition.left.toString())); + ColumnIdentifier column = condition.columnExpression().identifiers().get(0); + casReadConditionQuery.append(column.toString()); + casConditionIndex.add(getDataSpecification().partitionGenerator.indexOf(column.toString())); first = false; } casReadConditionQuery.append(" FROM ").append(tableName).append(" WHERE ");