diff --git a/CHANGES.txt b/CHANGES.txt index 74ba07ebdc..5b71af115f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,6 @@ 3.6 + * Support for non-frozen user-defined types, updating + individual fields of user-defined types (CASSANDRA-7423) * Make LZ4 compression level configurable (CASSANDRA-11051) * Allow per-partition LIMIT clause in CQL (CASSANDRA-7017) * Make custom filtering more extensible with UserExpression (CASSANDRA-11295) diff --git a/bin/cqlsh.py b/bin/cqlsh.py index c3fcc48d99..25934865bc 100644 --- a/bin/cqlsh.py +++ b/bin/cqlsh.py @@ -898,8 +898,7 @@ class Shell(cmd.Cmd): except KeyError: raise UserTypeNotFound("User type %r not found" % typename) - return [(field_name, field_type.cql_parameterized_type()) - for field_name, field_type in zip(user_type.field_names, user_type.field_types)] + return zip(user_type.field_names, user_type.field_types) def get_userfunction_names(self, ksname=None): if ksname is None: diff --git a/doc/cql3/CQL.textile b/doc/cql3/CQL.textile index 83899d2f16..b0173a646e 100644 --- a/doc/cql3/CQL.textile +++ b/doc/cql3/CQL.textile @@ -870,13 +870,17 @@ bc(syntax).. | '=' ('+' | '-') ( | | ) | '=' '+' | '[' ']' '=' + | '.' '=' ::= - | IN ( | '(' ( ( ',' )* )? ')') + | IN | '[' ']' - | '[' ']' IN + | '[' ']' IN + | '.' + | '.' IN ::= '<' | '<=' | '=' | '!=' | '>=' | '>' + ::= ( | '(' ( ( ',' )* )? ')') ::= ( AND )* @@ -913,6 +917,8 @@ The @c = c + 3@ form of @@ is used to increment/decrement counters. The @id = id + @ and @id[value1] = value2@ forms of @@ are for collections. Please refer to the "relevant section":#collections for more details. +The @id.field = @ form of @@ is for setting the value of a single field on a non-frozen user-defined types. + h4(#updateOptions). @@ The @UPDATE@ and @INSERT@ statements support the following options: @@ -931,7 +937,9 @@ bc(syntax).. WHERE ( IF ( EXISTS | ( ( AND )*) ) )? - ::= ( '[' ']' )? + ::= + | '[' ']' + | '.' ::= ( AND )* @@ -943,11 +951,14 @@ bc(syntax).. | '(' (',' )* ')' IN ::= '=' | '<' | '>' | '<=' | '>=' + ::= ( | '(' ( ( ',' )* )? ')') ::= ( | '!=') - | IN ( | '(' ( ( ',' )* )? ')') + | IN | '[' ']' ( | '!=') - | '[' ']' IN + | '[' ']' IN + | '.' ( | '!=') + | '.' IN p. __Sample:__ @@ -957,7 +968,7 @@ DELETE FROM NerdMovies USING TIMESTAMP 1240003134 WHERE movie = 'Serenity'; DELETE phone FROM Users WHERE userid IN (C73DE1D3-AF08-40F3-B124-3FF3E5109F22, B70DE1D0-9908-4AE3-BE34-5573E5B09F14); p. -The @DELETE@ statement deletes columns and rows. If column names are provided directly after the @DELETE@ keyword, only those columns are deleted from the row indicated by the @@ (the @id[value]@ syntax in @@ is for collection, please refer to the "collection section":#collections for more details). Otherwise, whole rows are removed. The @@ specifies which rows are to be deleted. Multiple rows may be deleted with one statement by using an @IN@ clause. A range of rows may be deleted using an inequality operator (such as @>=@). +The @DELETE@ statement deletes columns and rows. If column names are provided directly after the @DELETE@ keyword, only those columns are deleted from the row indicated by the @@. The @id[value]@ syntax in @@ is for non-frozen collections (please refer to the "collection section":#collections for more details). The @id.field@ syntax is for the deletion of non-frozen user-defined types. Otherwise, whole rows are removed. The @@ specifies which rows are to be deleted. Multiple rows may be deleted with one statement by using an @IN@ clause. A range of rows may be deleted using an inequality operator (such as @>=@). @DELETE@ supports the @TIMESTAMP@ option with the same semantics as the "@UPDATE@":#updateStmt statement. @@ -2318,6 +2329,10 @@ h3. 3.4.2 * "@ALTER TABLE@":#alterTableStmt @ADD@ and @DROP@ now allow mutiple columns to be added/removed * New "@PER PARTITION LIMIT@":#selectLimit option (see "CASSANDRA-7017":https://issues.apache.org/jira/browse/CASSANDRA-7017). +h3. 3.4.2 + +* User-defined types may now be stored in a non-frozen form, allowing individual fields to be updated and deleted in "@UPDATE@ statements":#updateStmt and "@DELETE@ statements":#deleteStmt, respectively. (CASSANDRA-7423) + h3. 3.4.1 * Adds @CAST@ functions. See "@Cast@":#castFun. diff --git a/pylib/cqlshlib/cql3handling.py b/pylib/cqlshlib/cql3handling.py index 673234bd74..7abed2c88f 100644 --- a/pylib/cqlshlib/cql3handling.py +++ b/pylib/cqlshlib/cql3handling.py @@ -552,13 +552,13 @@ def ks_name_completer(ctxt, cass): @completer_for('nonSystemKeyspaceName', 'ksname') -def ks_name_completer(ctxt, cass): +def non_system_ks_name_completer(ctxt, cass): ksnames = [n for n in cass.get_keyspace_names() if n not in SYSTEM_KEYSPACES] return map(maybe_escape_name, ksnames) @completer_for('alterableKeyspaceName', 'ksname') -def ks_name_completer(ctxt, cass): +def alterable_ks_name_completer(ctxt, cass): ksnames = [n for n in cass.get_keyspace_names() if n not in NONALTERBALE_KEYSPACES] return map(maybe_escape_name, ksnames) @@ -864,7 +864,6 @@ def insert_newval_completer(ctxt, cass): @completer_for('insertStatement', 'valcomma') def insert_valcomma_completer(ctxt, cass): - layout = get_table_meta(ctxt, cass) numcols = len(ctxt.get_binding('colname', ())) numvals = len(ctxt.get_binding('newval', ())) if numcols > numvals: @@ -888,21 +887,27 @@ syntax_rules += r''' ( "IF" ( "EXISTS" | ))? ; ::= updatecol= - ( "=" update_rhs=( | ) + (( "=" update_rhs=( | ) ( counterop=( "+" | "-" ) inc= - | listadder="+" listcol= )? - | indexbracket="[" "]" "=" ) + | listadder="+" listcol= )? ) + | ( indexbracket="[" "]" "=" ) + | ( udt_field_dot="." udt_field= "=" )) ; ::= ( "AND" )* ; - ::= ( "[" "]" )? (("=" | "<" | ">" | "<=" | ">=" | "!=") - | "IN" "(" ( "," )* ")") + ::= (("=" | "<" | ">" | "<=" | ">=" | "!=") ) + | ("IN" "(" ( "," )* ")" ) + ; + ::= conditioncol= + ( (( indexbracket="[" "]" ) + |( udt_field_dot="." udt_field= )) )? + ; ''' @completer_for('updateStatement', 'updateopt') -def insert_option_completer(ctxt, cass): +def update_option_completer(ctxt, cass): opts = set('TIMESTAMP TTL'.split()) for opt in ctxt.get_binding('updateopt', ()): opts.discard(opt.split()[0]) @@ -971,6 +976,62 @@ def update_indexbracket_completer(ctxt, cass): return ['['] return [] + +@completer_for('assignment', 'udt_field_dot') +def update_udt_field_dot_completer(ctxt, cass): + layout = get_table_meta(ctxt, cass) + curcol = dequote_name(ctxt.get_binding('updatecol', '')) + return ["."] if _is_usertype(layout, curcol) else [] + + +@completer_for('assignment', 'udt_field') +def assignment_udt_field_completer(ctxt, cass): + layout = get_table_meta(ctxt, cass) + curcol = dequote_name(ctxt.get_binding('updatecol', '')) + return _usertype_fields(ctxt, cass, layout, curcol) + + +def _is_usertype(layout, curcol): + coltype = layout.columns[curcol].cql_type + return coltype not in simple_cql_types and coltype not in ('map', 'set', 'list') + + +def _usertype_fields(ctxt, cass, layout, curcol): + if not _is_usertype(layout, curcol): + return [] + + coltype = layout.columns[curcol].cql_type + ks = ctxt.get_binding('ksname', None) + if ks is not None: + ks = dequote_name(ks) + user_type = cass.get_usertype_layout(ks, coltype) + return [field_name for (field_name, field_type) in user_type] + + +@completer_for('condition', 'indexbracket') +def condition_indexbracket_completer(ctxt, cass): + layout = get_table_meta(ctxt, cass) + curcol = dequote_name(ctxt.get_binding('conditioncol', '')) + coltype = layout.columns[curcol].cql_type + if coltype in ('map', 'list'): + return ['['] + return [] + + +@completer_for('condition', 'udt_field_dot') +def condition_udt_field_dot_completer(ctxt, cass): + layout = get_table_meta(ctxt, cass) + curcol = dequote_name(ctxt.get_binding('conditioncol', '')) + return ["."] if _is_usertype(layout, curcol) else [] + + +@completer_for('condition', 'udt_field') +def condition_udt_field_completer(ctxt, cass): + layout = get_table_meta(ctxt, cass) + curcol = dequote_name(ctxt.get_binding('conditioncol', '')) + return _usertype_fields(ctxt, cass, layout, curcol) + + syntax_rules += r''' ::= "DELETE" ( ( "," )* )? "FROM" cf= @@ -978,7 +1039,9 @@ syntax_rules += r''' "WHERE" ( "IF" ( "EXISTS" | ) )? ; - ::= delcol= ( memberbracket="[" memberselector= "]" )? + ::= delcol= + ( ( "[" "]" ) + | ( "." ) )? ; ::= "TIMESTAMP" ; @@ -998,6 +1061,7 @@ def delete_delcol_completer(ctxt, cass): layout = get_table_meta(ctxt, cass) return map(maybe_escape_name, regular_column_names(layout)) + syntax_rules += r''' ::= "BEGIN" ( "UNLOGGED" | "COUNTER" )? "BATCH" ( "USING" [batchopt]= @@ -1459,7 +1523,7 @@ def get_trigger_names(ctxt, cass): @completer_for('dropTriggerStatement', 'triggername') -def alter_type_field_completer(ctxt, cass): +def drop_trigger_completer(ctxt, cass): names = get_trigger_names(ctxt, cass) return map(maybe_escape_name, names) diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py index 0f0cc4d017..1f1fb48c51 100644 --- a/pylib/cqlshlib/test/test_cqlsh_completion.py +++ b/pylib/cqlshlib/test/test_cqlsh_completion.py @@ -367,12 +367,12 @@ class TestCqlshCompletion(CqlshCompletionCase): choices=['EXISTS', '', '']) self.trycompletions("UPDATE empty_table SET lonelycol = 'eggs' WHERE TOKEN(lonelykey) <= TOKEN(13) IF EXISTS ", - choices=['>=', '!=', '<=', 'IN', '[', ';', '=', '<', '>']) + choices=['>=', '!=', '<=', 'IN', '[', ';', '=', '<', '>', '.']) def test_complete_in_delete(self): self.trycompletions('DELETE F', choices=['FROM', '', '']) - self.trycompletions('DELETE a ', choices=['FROM', '[', ',']) + self.trycompletions('DELETE a ', choices=['FROM', '[', '.', ',']) self.trycompletions('DELETE a [', choices=['', 'false', '-', '', '', '', 'TOKEN', @@ -449,7 +449,7 @@ class TestCqlshCompletion(CqlshCompletionCase): choices=['EXISTS', '', '']) self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE ' 'TOKEN(a) >= TOKEN(0) IF b '), - choices=['>=', '!=', '<=', 'IN', '[', '=', '<', '>']) + choices=['>=', '!=', '<=', 'IN', '=', '<', '>']) self.trycompletions(('DELETE FROM twenty_rows_composite_table USING TIMESTAMP 0 WHERE ' 'TOKEN(a) >= TOKEN(0) IF b < 0 '), choices=['AND', ';']) diff --git a/src/antlr/Parser.g b/src/antlr/Parser.g index 36d4e2054a..cd17475571 100644 --- a/src/antlr/Parser.g +++ b/src/antlr/Parser.g @@ -429,6 +429,7 @@ deleteSelection returns [List operations] deleteOp returns [Operation.RawDeletion op] : c=cident { $op = new Operation.ColumnDeletion(c); } | c=cident '[' t=term ']' { $op = new Operation.ElementDeletion(c, t); } + | c=cident '.' field=cident { $op = new Operation.FieldDeletion(c, field); } ; usingClauseDelete[Attributes.Raw attrs] @@ -1282,7 +1283,8 @@ columnOperation[List> operations columnOperationDifferentiator[List> operations, ColumnIdentifier.Raw key] : '=' normalColumnOperation[operations, key] - | '[' k=term ']' specializedColumnOperation[operations, key, k] + | '[' k=term ']' collectionColumnOperation[operations, key, k] + | '.' field=cident udtColumnOperation[operations, key, field] ; normalColumnOperation[List> operations, ColumnIdentifier.Raw key] @@ -1315,13 +1317,20 @@ normalColumnOperation[List> oper } ; -specializedColumnOperation[List> operations, ColumnIdentifier.Raw key, Term.Raw k] +collectionColumnOperation[List> operations, ColumnIdentifier.Raw key, Term.Raw k] : '=' t=term { addRawUpdate(operations, key, new Operation.SetElement(k, t)); } ; +udtColumnOperation[List> operations, ColumnIdentifier.Raw key, ColumnIdentifier.Raw field] + : '=' t=term + { + addRawUpdate(operations, key, new Operation.SetField(field, t)); + } + ; + columnCondition[List> conditions] // Note: we'll reject duplicates later : key=cident @@ -1337,6 +1346,13 @@ columnCondition[List> conditions | marker=inMarker { conditions.add(Pair.create(key, ColumnCondition.Raw.collectionInCondition(element, marker))); } ) ) + | '.' field=cident + ( 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))); } + ) + ) ) ; diff --git a/src/java/org/apache/cassandra/config/ColumnDefinition.java b/src/java/org/apache/cassandra/config/ColumnDefinition.java index 6bcc2e01b7..2c2cbb72cc 100644 --- a/src/java/org/apache/cassandra/config/ColumnDefinition.java +++ b/src/java/org/apache/cassandra/config/ColumnDefinition.java @@ -164,10 +164,13 @@ public class ColumnDefinition extends ColumnSpecification implements Comparable< private static Comparator makeCellPathComparator(Kind kind, AbstractType type) { - if (kind.isPrimaryKeyKind() || !type.isCollection() || !type.isMultiCell()) + if (kind.isPrimaryKeyKind() || !type.isMultiCell()) return null; - CollectionType collection = (CollectionType) type; + AbstractType nameComparator = type.isCollection() + ? ((CollectionType) type).nameComparator() + : ((UserType) type).nameComparator(); + return new Comparator() { @@ -184,7 +187,7 @@ public class ColumnDefinition extends ColumnSpecification implements Comparable< // This will get more complicated once we have non-frozen UDT and nested collections assert path1.size() == 1 && path2.size() == 1; - return collection.nameComparator().compare(path1.get(0), path2.get(0)); + return nameComparator.compare(path1.get(0), path2.get(0)); } }; } @@ -365,8 +368,11 @@ public class ColumnDefinition extends ColumnSpecification implements Comparable< if (!isComplex()) throw new MarshalException("Only complex cells should have a cell path"); - assert type instanceof CollectionType; - ((CollectionType)type).nameComparator().validate(path.get(0)); + assert type.isMultiCell(); + if (type.isCollection()) + ((CollectionType)type).nameComparator().validate(path.get(0)); + else + ((UserType)type).nameComparator().validate(path.get(0)); } public static String toCQLString(Iterable defs) diff --git a/src/java/org/apache/cassandra/cql3/AbstractMarker.java b/src/java/org/apache/cassandra/cql3/AbstractMarker.java index d2bc022b0c..e490bbf360 100644 --- a/src/java/org/apache/cassandra/cql3/AbstractMarker.java +++ b/src/java/org/apache/cassandra/cql3/AbstractMarker.java @@ -67,16 +67,26 @@ public abstract class AbstractMarker extends Term.NonTerminal public NonTerminal prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException { - if (!(receiver.type instanceof CollectionType)) - return new Constants.Marker(bindIndex, receiver); - - switch (((CollectionType)receiver.type).kind) + if (receiver.type.isCollection()) { - case LIST: return new Lists.Marker(bindIndex, receiver); - case SET: return new Sets.Marker(bindIndex, receiver); - case MAP: return new Maps.Marker(bindIndex, receiver); + switch (((CollectionType) receiver.type).kind) + { + case LIST: + return new Lists.Marker(bindIndex, receiver); + case SET: + return new Sets.Marker(bindIndex, receiver); + case MAP: + return new Maps.Marker(bindIndex, receiver); + default: + throw new AssertionError(); + } } - throw new AssertionError(); + else if (receiver.type.isUDT()) + { + return new UserTypes.Marker(bindIndex, receiver); + } + + return new Constants.Marker(bindIndex, receiver); } public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver) diff --git a/src/java/org/apache/cassandra/cql3/CQL3Type.java b/src/java/org/apache/cassandra/cql3/CQL3Type.java index 98c0d9779d..d5dfeed6be 100644 --- a/src/java/org/apache/cassandra/cql3/CQL3Type.java +++ b/src/java/org/apache/cassandra/cql3/CQL3Type.java @@ -39,7 +39,16 @@ public interface CQL3Type { static final Logger logger = LoggerFactory.getLogger(CQL3Type.class); - public boolean isCollection(); + default boolean isCollection() + { + return false; + } + + default boolean isUDT() + { + return false; + } + public AbstractType getType(); /** @@ -82,11 +91,6 @@ public interface CQL3Type this.type = type; } - public boolean isCollection() - { - return false; - } - public AbstractType getType() { return type; @@ -125,11 +129,6 @@ public interface CQL3Type this(TypeParser.parse(className)); } - public boolean isCollection() - { - return false; - } - public AbstractType getType() { return type; @@ -305,9 +304,9 @@ public interface CQL3Type return new UserDefined(UTF8Type.instance.compose(type.name), type); } - public boolean isCollection() + public boolean isUDT() { - return false; + return true; } public AbstractType getType() @@ -377,7 +376,10 @@ public interface CQL3Type @Override public String toString() { - return "frozen<" + ColumnIdentifier.maybeQuote(name) + '>'; + if (type.isMultiCell()) + return ColumnIdentifier.maybeQuote(name); + else + return "frozen<" + ColumnIdentifier.maybeQuote(name) + '>'; } } @@ -395,11 +397,6 @@ public interface CQL3Type return new Tuple(type); } - public boolean isCollection() - { - return false; - } - public AbstractType getType() { return type; @@ -485,12 +482,7 @@ public interface CQL3Type { protected boolean frozen = false; - protected abstract boolean supportsFreezing(); - - public boolean isCollection() - { - return false; - } + public abstract boolean supportsFreezing(); public boolean isFrozen() { @@ -507,6 +499,11 @@ public interface CQL3Type return false; } + public boolean isUDT() + { + return false; + } + public String keyspace() { return null; @@ -588,7 +585,7 @@ public interface CQL3Type return type; } - protected boolean supportsFreezing() + public boolean supportsFreezing() { return false; } @@ -627,7 +624,7 @@ public interface CQL3Type frozen = true; } - protected boolean supportsFreezing() + public boolean supportsFreezing() { return true; } @@ -652,7 +649,7 @@ public interface CQL3Type assert values != null : "Got null values type for a collection"; if (!frozen && values.supportsFreezing() && !values.frozen) - throw new InvalidRequestException("Non-frozen collections are not allowed inside collections: " + this); + throwNestedNonFrozenError(values); // we represent Thrift supercolumns as maps, internally, and we do allow counters in supercolumns. Thus, // for internal type parsing (think schema) we have to make an exception and allow counters as (map) values @@ -664,22 +661,31 @@ public interface CQL3Type if (keys.isCounter()) throw new InvalidRequestException("Counters are not allowed inside collections: " + this); if (!frozen && keys.supportsFreezing() && !keys.frozen) - throw new InvalidRequestException("Non-frozen collections are not allowed inside collections: " + this); + throwNestedNonFrozenError(keys); } + AbstractType valueType = values.prepare(keyspace, udts).getType(); switch (kind) { case LIST: - return new Collection(ListType.getInstance(values.prepare(keyspace, udts).getType(), !frozen)); + return new Collection(ListType.getInstance(valueType, !frozen)); case SET: - return new Collection(SetType.getInstance(values.prepare(keyspace, udts).getType(), !frozen)); + return new Collection(SetType.getInstance(valueType, !frozen)); case MAP: assert keys != null : "Got null keys type for a collection"; - return new Collection(MapType.getInstance(keys.prepare(keyspace, udts).getType(), values.prepare(keyspace, udts).getType(), !frozen)); + return new Collection(MapType.getInstance(keys.prepare(keyspace, udts).getType(), valueType, !frozen)); } throw new AssertionError(); } + private void throwNestedNonFrozenError(Raw innerType) + { + if (innerType instanceof RawCollection) + throw new InvalidRequestException("Non-frozen collections are not allowed inside collections: " + this); + else + throw new InvalidRequestException("Non-frozen UDTs are not allowed inside collections: " + this); + } + public boolean referencesUserType(String name) { return (keys != null && keys.referencesUserType(name)) || values.referencesUserType(name); @@ -721,7 +727,7 @@ public interface CQL3Type public boolean canBeNonFrozen() { - return false; + return true; } public CQL3Type prepare(String keyspace, Types udts) throws InvalidRequestException @@ -744,9 +750,8 @@ public interface CQL3Type if (type == null) throw new InvalidRequestException("Unknown type " + name); - if (!frozen) - throw new InvalidRequestException("Non-frozen User-Defined types are not supported, please use frozen<>"); - + if (frozen) + type = type.freeze(); return new UserDefined(name.toString(), type); } @@ -755,7 +760,12 @@ public interface CQL3Type return this.name.getStringTypeName().equals(name); } - protected boolean supportsFreezing() + public boolean supportsFreezing() + { + return true; + } + + public boolean isUDT() { return true; } @@ -776,7 +786,7 @@ public interface CQL3Type this.types = types; } - protected boolean supportsFreezing() + public boolean supportsFreezing() { return true; } diff --git a/src/java/org/apache/cassandra/cql3/ColumnCondition.java b/src/java/org/apache/cassandra/cql3/ColumnCondition.java index 0c2ea2a54f..8db0ecc897 100644 --- a/src/java/org/apache/cassandra/cql3/ColumnCondition.java +++ b/src/java/org/apache/cassandra/cql3/ColumnCondition.java @@ -23,6 +23,7 @@ import java.util.*; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; +import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.db.rows.*; @@ -31,8 +32,6 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.transport.Server; import org.apache.cassandra.utils.ByteBufferUtil; -import static com.google.common.collect.Lists.newArrayList; - /** * A CQL3 condition on the value of a column or collection element. For example, "UPDATE .. IF a = 0". */ @@ -43,51 +42,71 @@ public class ColumnCondition // For collection, when testing the equality of a specific element, null otherwise. private final Term collectionElement; + // For UDT, when testing the equality of a specific field, null otherwise. + private final ColumnIdentifier field; + private final Term value; // a single value or a marker for a list of IN values private final List inValues; public final Operator operator; - private ColumnCondition(ColumnDefinition column, Term collectionElement, Term value, List inValues, Operator op) + private ColumnCondition(ColumnDefinition column, Term collectionElement, ColumnIdentifier field, Term value, List inValues, Operator op) { this.column = column; this.collectionElement = collectionElement; + this.field = field; this.value = value; this.inValues = inValues; this.operator = op; + assert field == null || collectionElement == null; if (operator != Operator.IN) assert this.inValues == null; } public static ColumnCondition condition(ColumnDefinition column, Term value, Operator op) { - return new ColumnCondition(column, null, value, null, op); + return new ColumnCondition(column, null, null, value, null, op); } public static ColumnCondition condition(ColumnDefinition column, Term collectionElement, Term value, Operator op) { - return new ColumnCondition(column, collectionElement, value, null, op); + return new ColumnCondition(column, collectionElement, null, value, null, op); + } + + public static ColumnCondition condition(ColumnDefinition column, ColumnIdentifier udtField, Term value, Operator op) + { + return new ColumnCondition(column, null, udtField, value, null, op); } public static ColumnCondition inCondition(ColumnDefinition column, List inValues) { - return new ColumnCondition(column, null, null, inValues, Operator.IN); + return new ColumnCondition(column, null, null, null, inValues, Operator.IN); } public static ColumnCondition inCondition(ColumnDefinition column, Term collectionElement, List inValues) { - return new ColumnCondition(column, collectionElement, null, inValues, Operator.IN); + return new ColumnCondition(column, collectionElement, null, null, inValues, Operator.IN); + } + + public static ColumnCondition inCondition(ColumnDefinition column, ColumnIdentifier udtField, List inValues) + { + return new ColumnCondition(column, null, udtField, null, inValues, Operator.IN); } public static ColumnCondition inCondition(ColumnDefinition column, Term inMarker) { - return new ColumnCondition(column, null, inMarker, null, Operator.IN); + return new ColumnCondition(column, null, null, inMarker, null, Operator.IN); } public static ColumnCondition inCondition(ColumnDefinition column, Term collectionElement, Term inMarker) { - return new ColumnCondition(column, collectionElement, inMarker, null, Operator.IN); + return new ColumnCondition(column, collectionElement, null, inMarker, null, Operator.IN); + } + + public static ColumnCondition inCondition(ColumnDefinition column, ColumnIdentifier udtField, Term inMarker) + { + return new ColumnCondition(column, null, udtField, inMarker, null, Operator.IN); } public Iterable getFunctions() @@ -131,11 +150,19 @@ public class ColumnCondition boolean isInCondition = operator == Operator.IN; if (column.type instanceof CollectionType) { - if (collectionElement == null) - return isInCondition ? new CollectionInBound(this, options) : new CollectionBound(this, options); - else + if (collectionElement != null) return isInCondition ? new ElementAccessInBound(this, options) : new ElementAccessBound(this, options); + else + return isInCondition ? new CollectionInBound(this, options) : new CollectionBound(this, options); } + else if (column.type.isUDT()) + { + if (field != null) + return isInCondition ? new UDTFieldAccessInBound(this, options) : new UDTFieldAccessBound(this, options); + else + return isInCondition ? new UDTInBound(this, options) : new UDTBound(this, options); + } + return isInCondition ? new SimpleInBound(this, options) : new SimpleBound(this, options); } @@ -216,6 +243,35 @@ public class ColumnCondition return complexData == null ? Collections.emptyIterator() : complexData.iterator(); } + private 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(); + } + } + + private static ByteBuffer cellValueAtIndex(Iterator iter, int index) + { + int adv = Iterators.advance(iter, index); + if (adv == index && iter.hasNext()) + return iter.next().value(); + else + return null; + } + /** * A condition on a single non-collection column. This does not support IN operators (see SimpleInBound). */ @@ -226,7 +282,7 @@ public class ColumnCondition private SimpleBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException { super(condition.column, condition.operator); - assert !(column.type instanceof CollectionType) && condition.collectionElement == null; + assert !(column.type instanceof CollectionType) && condition.field == null; assert condition.operator != Operator.IN; this.value = condition.value.bindAndGet(options); } @@ -247,7 +303,7 @@ public class ColumnCondition private SimpleInBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException { super(condition.column, condition.operator); - assert !(column.type instanceof CollectionType) && condition.collectionElement == null; + assert !(column.type instanceof CollectionType) && condition.field == null; assert condition.operator == Operator.IN; if (condition.inValues == null) this.inValues = ((Lists.Value) condition.value.bind(options)).getElements(); @@ -311,7 +367,7 @@ public class ColumnCondition ListType listType = (ListType) column.type; if (column.type.isMultiCell()) { - ByteBuffer columnValue = getListItem(getCells(row, column), getListIndex(collectionElement)); + ByteBuffer columnValue = cellValueAtIndex(getCells(row, column), getListIndex(collectionElement)); return compareWithOperator(operator, ((ListType)column.type).getElementsType(), value, columnValue); } else @@ -330,15 +386,6 @@ public class ColumnCondition return idx; } - static ByteBuffer getListItem(Iterator iter, int index) - { - int adv = Iterators.advance(iter, index); - if (adv == index && iter.hasNext()) - return iter.next().value(); - else - return null; - } - public ByteBuffer getCollectionElementValue() { return collectionElement; @@ -371,71 +418,46 @@ public class ColumnCondition if (collectionElement == null) throw new InvalidRequestException("Invalid null value for " + (column.type instanceof MapType ? "map" : "list") + " element access"); + ByteBuffer cellValue; + AbstractType valueType; if (column.type instanceof MapType) { MapType mapType = (MapType) column.type; - AbstractType valueType = mapType.getValuesType(); + valueType = mapType.getValuesType(); if (column.type.isMultiCell()) { - Cell item = getCell(row, column, CellPath.create(collectionElement)); - for (ByteBuffer value : inValues) - { - if (isSatisfiedByValue(value, item, valueType, Operator.EQ)) - return true; - } - return false; + Cell cell = getCell(row, column, CellPath.create(collectionElement)); + cellValue = cell == null ? null : cell.value(); } else { Cell cell = getCell(row, column); - ByteBuffer mapElementValue = cell == null - ? null - : mapType.getSerializer().getSerializedValue(cell.value(), collectionElement, mapType.getKeysType()); - for (ByteBuffer value : inValues) - { - if (value == null) - { - if (mapElementValue == null) - return true; - continue; - } - if (valueType.compare(value, mapElementValue) == 0) - return true; - } - return false; + cellValue = cell == null + ? null + : mapType.getSerializer().getSerializedValue(cell.value(), collectionElement, mapType.getKeysType()); + } + } + else // ListType + { + ListType listType = (ListType) column.type; + valueType = listType.getElementsType(); + if (column.type.isMultiCell()) + { + cellValue = cellValueAtIndex(getCells(row, column), ElementAccessBound.getListIndex(collectionElement)); + } + else + { + Cell cell = getCell(row, column); + cellValue = cell == null + ? null + : listType.getSerializer().getElement(cell.value(), ElementAccessBound.getListIndex(collectionElement)); } } - ListType listType = (ListType) column.type; - AbstractType elementsType = listType.getElementsType(); - if (column.type.isMultiCell()) + for (ByteBuffer value : inValues) { - ByteBuffer columnValue = ElementAccessBound.getListItem(getCells(row, column), ElementAccessBound.getListIndex(collectionElement)); - - for (ByteBuffer value : inValues) - { - if (compareWithOperator(Operator.EQ, elementsType, value, columnValue)) - return true; - } - } - else - { - Cell cell = getCell(row, column); - ByteBuffer listElementValue = cell == null - ? null - : listType.getSerializer().getElement(cell.value(), ElementAccessBound.getListIndex(collectionElement)); - - for (ByteBuffer value : inValues) - { - if (value == null) - { - if (listElementValue == null) - return true; - continue; - } - if (elementsType.compare(value, listElementValue) == 0) - return true; - } + if (compareWithOperator(Operator.EQ, valueType, value, cellValue)) + return true; } return false; } @@ -539,26 +561,6 @@ public class ColumnCondition return operator == Operator.EQ || operator == Operator.LTE || operator == Operator.GTE; } - private 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(); - } - } - static boolean listAppliesTo(ListType type, Iterator iter, List elements, Operator operator) { return setOrListAppliesTo(type.getElementsType(), iter, elements.iterator(), operator, false); @@ -691,6 +693,195 @@ public class ColumnCondition } } + /** A condition on a UDT field. IN operators are not supported here, see UDTFieldAccessInBound. */ + static class UDTFieldAccessBound extends Bound + { + public final ColumnIdentifier field; + public final ByteBuffer value; + + private UDTFieldAccessBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException + { + super(condition.column, condition.operator); + assert column.type.isUDT() && condition.field != null; + assert condition.operator != Operator.IN; + this.field = condition.field; + this.value = condition.value.bindAndGet(options); + } + + public boolean appliesTo(Row row) throws InvalidRequestException + { + UserType userType = (UserType) column.type; + int fieldPosition = userType.fieldPosition(field); + assert fieldPosition >= 0; + + ByteBuffer cellValue; + if (column.type.isMultiCell()) + { + Cell cell = getCell(row, column, userType.cellPathForField(field.bytes)); + cellValue = cell == null ? null : cell.value(); + } + else + { + Cell cell = getCell(row, column); + cellValue = cell == null + ? null + : userType.split(cell.value())[fieldPosition]; + } + return compareWithOperator(operator, userType.fieldType(fieldPosition), value, cellValue); + } + } + + /** An IN condition on a UDT field. For example: IF user.name IN ('a', 'b') */ + static class UDTFieldAccessInBound extends Bound + { + public final ColumnIdentifier field; + public final List inValues; + + private UDTFieldAccessInBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException + { + super(condition.column, condition.operator); + assert column.type.isUDT() && condition.field != null; + this.field = condition.field; + + if (condition.inValues == null) + this.inValues = ((Lists.Value) condition.value.bind(options)).getElements(); + else + { + this.inValues = new ArrayList<>(condition.inValues.size()); + for (Term value : condition.inValues) + this.inValues.add(value.bindAndGet(options)); + } + } + + public boolean appliesTo(Row row) throws InvalidRequestException + { + UserType userType = (UserType) column.type; + int fieldPosition = userType.fieldPosition(field); + assert fieldPosition >= 0; + + ByteBuffer cellValue; + if (column.type.isMultiCell()) + { + Cell cell = getCell(row, column, userType.cellPathForField(field.bytes)); + cellValue = cell == null ? null : cell.value(); + } + else + { + Cell cell = getCell(row, column); + cellValue = cell == null ? null : userType.split(getCell(row, column).value())[fieldPosition]; + } + + AbstractType valueType = userType.fieldType(fieldPosition); + for (ByteBuffer value : inValues) + { + if (compareWithOperator(Operator.EQ, valueType, value, cellValue)) + return true; + } + return false; + } + } + + /** A non-IN condition on an entire UDT. For example: IF user = {name: 'joe', age: 42}). */ + static class UDTBound extends Bound + { + private final ByteBuffer value; + private final int protocolVersion; + + private UDTBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException + { + super(condition.column, condition.operator); + assert column.type.isUDT() && condition.field == null; + assert condition.operator != Operator.IN; + protocolVersion = options.getProtocolVersion(); + value = condition.value.bindAndGet(options); + } + + public boolean appliesTo(Row row) throws InvalidRequestException + { + UserType userType = (UserType) column.type; + ByteBuffer rowValue; + if (userType.isMultiCell()) + { + Iterator iter = getCells(row, column); + rowValue = iter.hasNext() ? userType.serializeForNativeProtocol(iter, protocolVersion) : null; + } + else + { + Cell cell = getCell(row, column); + rowValue = cell == null ? null : cell.value(); + } + + if (value == null) + { + if (operator == Operator.EQ) + return rowValue == null; + else if (operator == Operator.NEQ) + return rowValue != null; + else + throw new InvalidRequestException(String.format("Invalid comparison with null for operator \"%s\"", operator)); + } + + return compareWithOperator(operator, userType, value, rowValue); + } + } + + /** An IN condition on an entire UDT. For example: IF user IN ({name: 'joe', age: 42}, {name: 'bob', age: 23}). */ + public static class UDTInBound extends Bound + { + private final List inValues; + private final int protocolVersion; + + private UDTInBound(ColumnCondition condition, QueryOptions options) throws InvalidRequestException + { + super(condition.column, condition.operator); + assert column.type.isUDT() && condition.field == null; + assert condition.operator == Operator.IN; + protocolVersion = options.getProtocolVersion(); + inValues = new ArrayList<>(); + if (condition.inValues == null) + { + Lists.Marker inValuesMarker = (Lists.Marker) condition.value; + for (ByteBuffer buffer : ((Lists.Value)inValuesMarker.bind(options)).elements) + this.inValues.add(buffer); + } + else + { + for (Term value : condition.inValues) + this.inValues.add(value.bindAndGet(options)); + } + } + + public boolean appliesTo(Row row) throws InvalidRequestException + { + UserType userType = (UserType) column.type; + ByteBuffer rowValue; + if (userType.isMultiCell()) + { + Iterator cells = getCells(row, column); + rowValue = cells.hasNext() ? userType.serializeForNativeProtocol(cells, protocolVersion) : null; + } + else + { + Cell cell = getCell(row, column); + rowValue = cell == null ? null : cell.value(); + } + + for (ByteBuffer value : inValues) + { + if (value == null || rowValue == null) + { + if (value == rowValue) // both null + return true; + } + else if (userType.compare(value, rowValue) == 0) + { + return true; + } + } + return false; + } + } + public static class Raw { private final Term.Raw value; @@ -700,59 +891,142 @@ public class ColumnCondition // 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 ColumnIdentifier.Raw udtField; + private final Operator operator; - private Raw(Term.Raw value, List inValues, AbstractMarker.INRaw inMarker, Term.Raw collectionElement, Operator op) + private Raw(Term.Raw value, List inValues, AbstractMarker.INRaw inMarker, Term.Raw collectionElement, + ColumnIdentifier.Raw udtField, Operator op) { this.value = value; this.inValues = inValues; this.inMarker = inMarker; this.collectionElement = collectionElement; + this.udtField = udtField; this.operator = op; } /** A condition on a column. For example: "IF col = 'foo'" */ public static Raw simpleCondition(Term.Raw value, Operator op) { - return new Raw(value, null, null, null, op); + return new Raw(value, null, null, null, null, op); } /** An IN condition on a column. For example: "IF col IN ('foo', 'bar', ...)" */ public static Raw simpleInCondition(List inValues) { - return new Raw(null, inValues, null, null, Operator.IN); + return new Raw(null, inValues, null, null, null, Operator.IN); } /** An IN condition on a column with a single marker. For example: "IF col IN ?" */ public static Raw simpleInCondition(AbstractMarker.INRaw inMarker) { - return new Raw(null, null, inMarker, null, Operator.IN); + return new Raw(null, null, inMarker, null, null, Operator.IN); } /** A condition on a collection element. For example: "IF col['key'] = 'foo'" */ public static Raw collectionCondition(Term.Raw value, Term.Raw collectionElement, Operator op) { - return new Raw(value, null, null, collectionElement, op); + return new Raw(value, null, null, collectionElement, null, op); } /** An IN condition on a collection element. For example: "IF col['key'] IN ('foo', 'bar', ...)" */ public static Raw collectionInCondition(Term.Raw collectionElement, List inValues) { - return new Raw(null, inValues, null, collectionElement, Operator.IN); + return new Raw(null, inValues, null, collectionElement, null, Operator.IN); } /** An IN condition on a collection element with a single marker. For example: "IF col['key'] IN ?" */ public static Raw collectionInCondition(Term.Raw collectionElement, AbstractMarker.INRaw inMarker) { - return new Raw(null, null, inMarker, collectionElement, Operator.IN); + return new Raw(null, null, inMarker, collectionElement, null, Operator.IN); } - public ColumnCondition prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + /** A condition on a UDT field. For example: "IF col.field = 'foo'" */ + public static Raw udtFieldCondition(Term.Raw value, ColumnIdentifier.Raw udtField, Operator op) + { + return new Raw(value, null, null, null, udtField, op); + } + + /** An IN condition on a collection element. For example: "IF col.field IN ('foo', 'bar', ...)" */ + public static Raw udtFieldInCondition(ColumnIdentifier.Raw udtField, List inValues) + { + return new Raw(null, inValues, null, null, udtField, Operator.IN); + } + + /** An IN condition on a collection element with a single marker. For example: "IF col.field IN ?" */ + public static Raw udtFieldInCondition(ColumnIdentifier.Raw udtField, AbstractMarker.INRaw inMarker) + { + return new Raw(null, null, inMarker, null, udtField, Operator.IN); + } + + public ColumnCondition prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException { if (receiver.type instanceof CounterColumnType) throw new InvalidRequestException("Conditions on counters are not supported"); - if (collectionElement == null) + if (collectionElement != null) + { + if (!(receiver.type.isCollection())) + throw new InvalidRequestException(String.format("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 new InvalidRequestException(String.format("Invalid element access syntax for set column %s", receiver.name)); + default: + throw new AssertionError(); + } + if (operator == Operator.IN) + { + if (inValues == null) + return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), inMarker.prepare(keyspace, valueSpec)); + List terms = new ArrayList<>(inValues.size()); + for (Term.Raw value : inValues) + terms.add(value.prepare(keyspace, valueSpec)); + return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), terms); + } + else + { + return ColumnCondition.condition(receiver, collectionElement.prepare(keyspace, elementSpec), value.prepare(keyspace, valueSpec), operator); + } + } + else if (udtField != null) + { + UserType userType = (UserType) receiver.type; + ColumnIdentifier fieldIdentifier = udtField.prepare(cfm); + + int fieldPosition = userType.fieldPosition(fieldIdentifier); + if (fieldPosition == -1) + throw new InvalidRequestException(String.format("Unknown field %s for column %s", fieldIdentifier, receiver.name)); + + ColumnSpecification fieldReceiver = UserTypes.fieldSpecOf(receiver, fieldPosition); + if (operator == Operator.IN) + { + if (inValues == null) + return ColumnCondition.inCondition(receiver, udtField.prepare(cfm), inMarker.prepare(keyspace, fieldReceiver)); + + List terms = new ArrayList<>(inValues.size()); + for (Term.Raw value : inValues) + terms.add(value.prepare(keyspace, fieldReceiver)); + return ColumnCondition.inCondition(receiver, udtField.prepare(cfm), terms); + } + else + { + return ColumnCondition.condition(receiver, udtField.prepare(cfm), value.prepare(keyspace, fieldReceiver), operator); + } + } + else { if (operator == Operator.IN) { @@ -768,39 +1042,6 @@ public class ColumnCondition return ColumnCondition.condition(receiver, value.prepare(keyspace, receiver), operator); } } - - if (!(receiver.type.isCollection())) - throw new InvalidRequestException(String.format("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 new InvalidRequestException(String.format("Invalid element access syntax for set column %s", receiver.name)); - default: - throw new AssertionError(); - } - if (operator == Operator.IN) - { - if (inValues == null) - return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), inMarker.prepare(keyspace, valueSpec)); - List terms = new ArrayList<>(inValues.size()); - for (Term.Raw value : inValues) - terms.add(value.prepare(keyspace, valueSpec)); - return ColumnCondition.inCondition(receiver, collectionElement.prepare(keyspace, elementSpec), terms); - } - else - { - return ColumnCondition.condition(receiver, collectionElement.prepare(keyspace, elementSpec), value.prepare(keyspace, valueSpec), operator); - } } } } diff --git a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java index 74d0d28c86..f2021459c4 100644 --- a/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java +++ b/src/java/org/apache/cassandra/cql3/ColumnIdentifier.java @@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.MapMaker; import org.apache.cassandra.cache.IMeasurableMemory; @@ -329,7 +330,8 @@ public class ColumnIdentifier extends Selectable implements IMeasurableMemory, C } } - static String maybeQuote(String text) + @VisibleForTesting + public static String maybeQuote(String text) { if (UNQUOTED_IDENTIFIER.matcher(text).matches()) return text; diff --git a/src/java/org/apache/cassandra/cql3/Constants.java b/src/java/org/apache/cassandra/cql3/Constants.java index 4982c490be..2efce59bea 100644 --- a/src/java/org/apache/cassandra/cql3/Constants.java +++ b/src/java/org/apache/cassandra/cql3/Constants.java @@ -162,7 +162,7 @@ public abstract class Constants public AssignmentTestable.TestResult testAssignment(String keyspace, ColumnSpecification receiver) { CQL3Type receiverType = receiver.type.asCQL3Type(); - if (receiverType.isCollection()) + if (receiverType.isCollection() || receiverType.isUDT()) return AssignmentTestable.TestResult.NOT_ASSIGNABLE; if (!(receiverType instanceof CQL3Type.Native)) diff --git a/src/java/org/apache/cassandra/cql3/Operation.java b/src/java/org/apache/cassandra/cql3/Operation.java index 51c8969074..bfb05e7875 100644 --- a/src/java/org/apache/cassandra/cql3/Operation.java +++ b/src/java/org/apache/cassandra/cql3/Operation.java @@ -19,6 +19,7 @@ package org.apache.cassandra.cql3; import java.util.Collections; +import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.cql3.functions.Function; import org.apache.cassandra.db.DecoratedKey; @@ -107,12 +108,10 @@ public abstract class Operation * It returns an Operation which can be though as post-preparation well-typed * Operation. * - * @param receiver the "column" this operation applies to. Note that - * contrarly to the method of same name in Term.Raw, the receiver should always - * be a true column. + * @param receiver the column this operation applies to. * @return the prepared update operation. */ - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException; + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException; /** * @return whether this operation can be applied alongside the {@code @@ -146,7 +145,7 @@ public abstract class Operation * @param receiver the "column" this operation applies to. * @return the prepared delete operation. */ - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException; + public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException; } public static class SetValue implements RawUpdate @@ -158,26 +157,32 @@ public abstract class Operation this.value = value; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException { - Term v = value.prepare(keyspace, receiver); + Term v = value.prepare(cfm.ksName, receiver); if (receiver.type instanceof CounterColumnType) throw new InvalidRequestException(String.format("Cannot set the value of counter column %s (counters can only be incremented/decremented, not set)", receiver.name)); - if (!(receiver.type.isCollection())) - return new Constants.Setter(receiver, v); - - switch (((CollectionType)receiver.type).kind) + if (receiver.type.isCollection()) { - case LIST: - return new Lists.Setter(receiver, v); - case SET: - return new Sets.Setter(receiver, v); - case MAP: - return new Maps.Setter(receiver, v); + switch (((CollectionType) receiver.type).kind) + { + case LIST: + return new Lists.Setter(receiver, v); + case SET: + return new Sets.Setter(receiver, v); + case MAP: + return new Maps.Setter(receiver, v); + default: + throw new AssertionError(); + } } - throw new AssertionError(); + + if (receiver.type.isUDT()) + return new UserTypes.Setter(receiver, v); + + return new Constants.Setter(receiver, v); } protected String toString(ColumnSpecification column) @@ -204,7 +209,7 @@ public abstract class Operation this.value = value; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException { if (!(receiver.type instanceof CollectionType)) throw new InvalidRequestException(String.format("Invalid operation (%s) for non collection column %s", toString(receiver), receiver.name)); @@ -214,14 +219,14 @@ public abstract class Operation switch (((CollectionType)receiver.type).kind) { case LIST: - Term idx = selector.prepare(keyspace, Lists.indexSpecOf(receiver)); - Term lval = value.prepare(keyspace, Lists.valueSpecOf(receiver)); + Term idx = selector.prepare(cfm.ksName, Lists.indexSpecOf(receiver)); + Term lval = value.prepare(cfm.ksName, Lists.valueSpecOf(receiver)); return new Lists.SetterByIndex(receiver, idx, lval); case SET: throw new InvalidRequestException(String.format("Invalid operation (%s) for set column %s", toString(receiver), receiver.name)); case MAP: - Term key = selector.prepare(keyspace, Maps.keySpecOf(receiver)); - Term mval = value.prepare(keyspace, Maps.valueSpecOf(receiver)); + Term key = selector.prepare(cfm.ksName, Maps.keySpecOf(receiver)); + Term mval = value.prepare(cfm.ksName, Maps.valueSpecOf(receiver)); return new Maps.SetterByKey(receiver, key, mval); } throw new AssertionError(); @@ -240,6 +245,47 @@ public abstract class Operation } } + public static class SetField implements RawUpdate + { + private final ColumnIdentifier.Raw field; + private final Term.Raw value; + + public SetField(ColumnIdentifier.Raw field, Term.Raw value) + { + this.field = field; + this.value = value; + } + + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException + { + if (!receiver.type.isUDT()) + throw new InvalidRequestException(String.format("Invalid operation (%s) for non-UDT column %s", toString(receiver), receiver.name)); + else if (!receiver.type.isMultiCell()) + throw new InvalidRequestException(String.format("Invalid operation (%s) for frozen UDT column %s", toString(receiver), receiver.name)); + + ColumnIdentifier fieldIdentifier = field.prepare(cfm); + int fieldPosition = ((UserType) receiver.type).fieldPosition(fieldIdentifier); + if (fieldPosition == -1) + throw new InvalidRequestException(String.format("UDT column %s does not have a field named %s", receiver.name, fieldIdentifier)); + + Term val = value.prepare(cfm.ksName, UserTypes.fieldSpecOf(receiver, fieldPosition)); + return new UserTypes.SetterByField(receiver, fieldIdentifier, val); + } + + protected String toString(ColumnSpecification column) + { + return String.format("%s.%s = %s", column.name, field, value); + } + + public boolean isCompatibleWith(RawUpdate other) + { + if (other instanceof SetField) + return !((SetField) other).field.equals(field); + else + return !(other instanceof SetValue); + } + } + public static class Addition implements RawUpdate { private final Term.Raw value; @@ -249,9 +295,9 @@ public abstract class Operation this.value = value; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException { - Term v = value.prepare(keyspace, receiver); + Term v = value.prepare(cfm.ksName, receiver); if (!(receiver.type instanceof CollectionType)) { @@ -294,13 +340,13 @@ public abstract class Operation this.value = value; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException { if (!(receiver.type instanceof CollectionType)) { if (!(receiver.type instanceof CounterColumnType)) throw new InvalidRequestException(String.format("Invalid operation (%s) for non counter column %s", toString(receiver), receiver.name)); - return new Constants.Substracter(receiver, value.prepare(keyspace, receiver)); + return new Constants.Substracter(receiver, value.prepare(cfm.ksName, receiver)); } else if (!(receiver.type.isMultiCell())) throw new InvalidRequestException(String.format("Invalid operation (%s) for frozen collection column %s", toString(receiver), receiver.name)); @@ -308,16 +354,16 @@ public abstract class Operation switch (((CollectionType)receiver.type).kind) { case LIST: - return new Lists.Discarder(receiver, value.prepare(keyspace, receiver)); + return new Lists.Discarder(receiver, value.prepare(cfm.ksName, receiver)); case SET: - return new Sets.Discarder(receiver, value.prepare(keyspace, receiver)); + return new Sets.Discarder(receiver, value.prepare(cfm.ksName, receiver)); case MAP: // The value for a map subtraction is actually a set ColumnSpecification vr = new ColumnSpecification(receiver.ksName, receiver.cfName, receiver.name, SetType.getInstance(((MapType)receiver.type).getKeysType(), false)); - return new Sets.Discarder(receiver, value.prepare(keyspace, vr)); + return new Sets.Discarder(receiver, value.prepare(cfm.ksName, vr)); } throw new AssertionError(); } @@ -342,9 +388,9 @@ public abstract class Operation this.value = value; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(CFMetaData cfm, ColumnDefinition receiver) throws InvalidRequestException { - Term v = value.prepare(keyspace, receiver); + Term v = value.prepare(cfm.ksName, receiver); if (!(receiver.type instanceof ListType)) throw new InvalidRequestException(String.format("Invalid operation (%s) for non list column %s", toString(receiver), receiver.name)); @@ -379,7 +425,7 @@ public abstract class Operation return id; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException { // No validation, deleting a column is always "well typed" return new Constants.Deleter(receiver); @@ -402,7 +448,7 @@ public abstract class Operation return id; } - public Operation prepare(String keyspace, ColumnDefinition receiver) throws InvalidRequestException + public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException { if (!(receiver.type.isCollection())) throw new InvalidRequestException(String.format("Invalid deletion operation for non collection column %s", receiver.name)); @@ -424,4 +470,35 @@ public abstract class Operation throw new AssertionError(); } } + + public static class FieldDeletion implements RawDeletion + { + private final ColumnIdentifier.Raw id; + private final ColumnIdentifier.Raw field; + + public FieldDeletion(ColumnIdentifier.Raw id, ColumnIdentifier.Raw field) + { + this.id = id; + this.field = field; + } + + public ColumnIdentifier.Raw affectedColumn() + { + return id; + } + + public Operation prepare(String keyspace, ColumnDefinition receiver, CFMetaData cfm) throws InvalidRequestException + { + if (!receiver.type.isUDT()) + throw new InvalidRequestException(String.format("Invalid field deletion operation for non-UDT column %s", receiver.name)); + else if (!receiver.type.isMultiCell()) + throw new InvalidRequestException(String.format("Frozen UDT column %s does not support field deletions", receiver.name)); + + ColumnIdentifier fieldIdentifier = field.prepare(cfm); + if (((UserType) receiver.type).fieldPosition(fieldIdentifier) == -1) + throw new InvalidRequestException(String.format("UDT column %s does not have a field named %s", receiver.name, fieldIdentifier)); + + return new UserTypes.DeleterByField(receiver, fieldIdentifier); + } + } } diff --git a/src/java/org/apache/cassandra/cql3/Tuples.java b/src/java/org/apache/cassandra/cql3/Tuples.java index 6c7df47918..ca4b6d970b 100644 --- a/src/java/org/apache/cassandra/cql3/Tuples.java +++ b/src/java/org/apache/cassandra/cql3/Tuples.java @@ -111,8 +111,10 @@ public class Tuples for (int i = 0; i < elements.size(); i++) { if (i >= tt.size()) + { throw new InvalidRequestException(String.format("Invalid tuple literal for %s: too many elements. Type %s expects %d but got %d", - receiver.name, tt.asCQL3Type(), tt.size(), elements.size())); + receiver.name, tt.asCQL3Type(), tt.size(), elements.size())); + } Term.Raw value = elements.get(i); ColumnSpecification spec = componentSpecOf(receiver, i); @@ -154,6 +156,13 @@ public class Tuples public static Value fromSerialized(ByteBuffer bytes, TupleType type) { + ByteBuffer[] values = type.split(bytes); + if (values.length > type.size()) + { + throw new InvalidRequestException(String.format( + "Tuple value contained too many fields (expected %s, got %s)", type.size(), values.length)); + } + return new Value(type.split(bytes)); } @@ -199,6 +208,10 @@ public class Tuples private ByteBuffer[] bindInternal(QueryOptions options) throws InvalidRequestException { + if (elements.size() > type.size()) + throw new InvalidRequestException(String.format( + "Tuple value contained too many fields (expected %s, got %s)", type.size(), elements.size())); + ByteBuffer[] buffers = new ByteBuffer[elements.size()]; for (int i = 0; i < elements.size(); i++) { diff --git a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java index fb8d567773..3d70051c36 100644 --- a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java +++ b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java @@ -245,7 +245,7 @@ public abstract class UntypedResultSet implements Iterable { ComplexColumnData complexData = row.getComplexColumnData(def); if (complexData != null) - data.put(def.name.toString(), ((CollectionType)def.type).serializeForNativeProtocol(def, complexData.iterator(), Server.VERSION_3)); + data.put(def.name.toString(), ((CollectionType)def.type).serializeForNativeProtocol(complexData.iterator(), Server.VERSION_3)); } } diff --git a/src/java/org/apache/cassandra/cql3/UserTypes.java b/src/java/org/apache/cassandra/cql3/UserTypes.java index 0beff065f1..db46fdd693 100644 --- a/src/java/org/apache/cassandra/cql3/UserTypes.java +++ b/src/java/org/apache/cassandra/cql3/UserTypes.java @@ -20,12 +20,18 @@ package org.apache.cassandra.cql3; import java.nio.ByteBuffer; import java.util.*; +import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.cql3.functions.Function; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.marshal.TupleType; import org.apache.cassandra.db.marshal.UTF8Type; import org.apache.cassandra.db.marshal.UserType; +import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.utils.ByteBufferUtil; +import static org.apache.cassandra.cql3.Constants.UNSET_VALUE; + /** * Static helper methods and classes for user types. */ @@ -78,8 +84,10 @@ public abstract class UserTypes { // We had some field that are not part of the type for (ColumnIdentifier id : entries.keySet()) + { if (!ut.fieldNames().contains(id.bytes)) throw new InvalidRequestException(String.format("Unknown field '%s' in value of user defined type %s", id, ut.getNameAsString())); + } } DelayedValue value = new DelayedValue(((UserType)receiver.type), values); @@ -88,7 +96,7 @@ public abstract class UserTypes private void validateAssignableTo(String keyspace, ColumnSpecification receiver) throws InvalidRequestException { - if (!(receiver.type instanceof UserType)) + if (!receiver.type.isUDT()) throw new InvalidRequestException(String.format("Invalid user type literal for %s of type %s", receiver, receiver.type.asCQL3Type())); UserType ut = (UserType)receiver.type; @@ -101,7 +109,10 @@ public abstract class UserTypes ColumnSpecification fieldSpec = fieldSpecOf(receiver, i); if (!value.testAssignment(keyspace, fieldSpec).isAssignable()) - throw new InvalidRequestException(String.format("Invalid user type literal for %s: field %s is not of type %s", receiver, field, fieldSpec.type.asCQL3Type())); + { + throw new InvalidRequestException(String.format("Invalid user type literal for %s: field %s is not of type %s", + receiver, field, fieldSpec.type.asCQL3Type())); + } } } @@ -135,7 +146,52 @@ public abstract class UserTypes } } - // Same purpose than Lists.DelayedValue, except we do handle bind marker in that case + public static class Value extends Term.MultiItemTerminal + { + private final UserType type; + public final ByteBuffer[] elements; + + public Value(UserType type, ByteBuffer[] elements) + { + this.type = type; + this.elements = elements; + } + + public static Value fromSerialized(ByteBuffer bytes, UserType type) + { + ByteBuffer[] values = type.split(bytes); + if (values.length > type.size()) + { + throw new InvalidRequestException(String.format( + "UDT value contained too many fields (expected %s, got %s)", type.size(), values.length)); + } + + return new Value(type, type.split(bytes)); + } + + public ByteBuffer get(int protocolVersion) + { + return TupleType.buildValue(elements); + } + + public boolean equals(UserType userType, Value v) + { + if (elements.length != v.elements.length) + return false; + + for (int i = 0; i < elements.length; i++) + if (userType.fieldType(i).compare(elements[i], v.elements[i]) != 0) + return false; + + return true; + } + + public List getElements() + { + return Arrays.asList(elements); + } + } + public static class DelayedValue extends Term.NonTerminal { private final UserType type; @@ -168,20 +224,27 @@ public abstract class UserTypes private ByteBuffer[] bindInternal(QueryOptions options) throws InvalidRequestException { + if (values.size() > type.size()) + { + throw new InvalidRequestException(String.format( + "UDT value contained too many fields (expected %s, got %s)", type.size(), values.size())); + } + ByteBuffer[] buffers = new ByteBuffer[values.size()]; for (int i = 0; i < type.size(); i++) { buffers[i] = values.get(i).bindAndGet(options); - // Since A UDT value is always written in its entirety Cassandra can't preserve a pre-existing value by 'not setting' the new value. Reject the query. - if (buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER) + // Since a frozen UDT value is always written in its entirety Cassandra can't preserve a pre-existing + // value by 'not setting' the new value. Reject the query. + if (!type.isMultiCell() && buffers[i] == ByteBufferUtil.UNSET_BYTE_BUFFER) throw new InvalidRequestException(String.format("Invalid unset value for field '%s' of user defined type %s", type.fieldNameAsString(i), type.getNameAsString())); } return buffers; } - public Constants.Value bind(QueryOptions options) throws InvalidRequestException + public Value bind(QueryOptions options) throws InvalidRequestException { - return new Constants.Value(bindAndGet(options)); + return new Value(type, bindInternal(options)); } @Override @@ -190,4 +253,113 @@ public abstract class UserTypes return UserType.buildValue(bindInternal(options)); } } + + public static class Marker extends AbstractMarker + { + protected Marker(int bindIndex, ColumnSpecification receiver) + { + super(bindIndex, receiver); + assert receiver.type.isUDT(); + } + + public Terminal bind(QueryOptions options) throws InvalidRequestException + { + ByteBuffer value = options.getValues().get(bindIndex); + if (value == null) + return null; + if (value == ByteBufferUtil.UNSET_BYTE_BUFFER) + return UNSET_VALUE; + return Value.fromSerialized(value, (UserType) receiver.type); + } + } + + public static class Setter extends Operation + { + public Setter(ColumnDefinition column, Term t) + { + super(column, t); + } + + public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException + { + Term.Terminal value = t.bind(params.options); + if (value == UNSET_VALUE) + return; + + Value userTypeValue = (Value) value; + if (column.type.isMultiCell()) + { + // setting a whole UDT at once means we overwrite all cells, so delete existing cells + params.setComplexDeletionTimeForOverwrite(column); + if (value == null) + return; + + Iterator fieldNameIter = userTypeValue.type.fieldNames().iterator(); + for (ByteBuffer buffer : userTypeValue.elements) + { + ByteBuffer fieldName = fieldNameIter.next(); + if (buffer == null) + continue; + + CellPath fieldPath = userTypeValue.type.cellPathForField(fieldName); + params.addCell(column, fieldPath, buffer); + } + } + else + { + // for frozen UDTs, we're overwriting the whole cell value + if (value == null) + params.addTombstone(column); + else + params.addCell(column, value.get(params.options.getProtocolVersion())); + } + } + } + + public static class SetterByField extends Operation + { + private final ColumnIdentifier field; + + public SetterByField(ColumnDefinition column, ColumnIdentifier field, Term t) + { + super(column, t); + this.field = field; + } + + public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException + { + // we should not get here for frozen UDTs + assert column.type.isMultiCell() : "Attempted to set an individual field on a frozen UDT"; + + Term.Terminal value = t.bind(params.options); + if (value == UNSET_VALUE) + return; + + CellPath fieldPath = ((UserType) column.type).cellPathForField(field.bytes); + if (value == null) + params.addTombstone(column, fieldPath); + else + params.addCell(column, fieldPath, value.get(params.options.getProtocolVersion())); + } + } + + public static class DeleterByField extends Operation + { + private final ColumnIdentifier field; + + public DeleterByField(ColumnDefinition column, ColumnIdentifier field) + { + super(column, null); + this.field = field; + } + + public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException + { + // we should not get here for frozen UDTs + assert column.type.isMultiCell() : "Attempted to delete a single field from a frozen UDT"; + + CellPath fieldPath = ((UserType) column.type).cellPathForField(field.bytes); + params.addTombstone(column, fieldPath); + } + } } diff --git a/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java b/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java index d36a492606..cdbc855127 100644 --- a/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java +++ b/src/java/org/apache/cassandra/cql3/functions/AbstractFunction.java @@ -91,7 +91,7 @@ public abstract class AbstractFunction implements Function // We should ignore the fact that the receiver type is frozen in our comparison as functions do not support // frozen types for return type AbstractType returnType = returnType(); - if (receiver.type.isFrozenCollection()) + if (receiver.type.isFreezable() && !receiver.type.isMultiCell()) returnType = returnType.freeze(); if (receiver.type.equals(returnType)) diff --git a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java index 1766a796bc..5021e10002 100644 --- a/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java +++ b/src/java/org/apache/cassandra/cql3/functions/FunctionCall.java @@ -99,16 +99,24 @@ public class FunctionCall extends Term.NonTerminal private static Term.Terminal makeTerminal(Function fun, ByteBuffer result, int version) throws InvalidRequestException { - if (!(fun.returnType() instanceof CollectionType)) - return new Constants.Value(result); - - switch (((CollectionType)fun.returnType()).kind) + if (fun.returnType().isCollection()) { - case LIST: return Lists.Value.fromSerialized(result, (ListType)fun.returnType(), version); - case SET: return Sets.Value.fromSerialized(result, (SetType)fun.returnType(), version); - case MAP: return Maps.Value.fromSerialized(result, (MapType)fun.returnType(), version); + switch (((CollectionType) fun.returnType()).kind) + { + case LIST: + return Lists.Value.fromSerialized(result, (ListType) fun.returnType(), version); + case SET: + return Sets.Value.fromSerialized(result, (SetType) fun.returnType(), version); + case MAP: + return Maps.Value.fromSerialized(result, (MapType) fun.returnType(), version); + } } - throw new AssertionError(); + else if (fun.returnType().isUDT()) + { + return UserTypes.Value.fromSerialized(result, (UserType) fun.returnType()); + } + + return new Constants.Value(result); } public static class Raw extends Term.Raw diff --git a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java index ade69dd79b..a38cc80ebf 100644 --- a/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java +++ b/src/java/org/apache/cassandra/cql3/functions/UDAggregate.java @@ -223,7 +223,7 @@ public class UDAggregate extends AbstractFunction implements AggregateFunction && Functions.typesMatch(returnType, that.returnType) && Objects.equal(stateFunction, that.stateFunction) && Objects.equal(finalFunction, that.finalFunction) - && Objects.equal(stateType, that.stateType) + && ((stateType == that.stateType) || ((stateType != null) && stateType.equals(that.stateType, true))) // ignore freezing && Objects.equal(initcond, that.initcond); } diff --git a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java index b00214c623..ba1e7c0cd3 100644 --- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java +++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java @@ -124,7 +124,7 @@ public final class StatementRestrictions WhereClause whereClause, VariableSpecifications boundNames, boolean selectsOnlyStaticColumns, - boolean selectACollection, + boolean selectsComplexColumn, boolean useFiltering, boolean forView) throws InvalidRequestException { @@ -218,7 +218,7 @@ public final class StatementRestrictions throw invalidRequest("Cannot restrict clustering columns when selecting only static columns"); } - processClusteringColumnsRestrictions(hasQueriableIndex, selectsOnlyStaticColumns, selectACollection, forView); + processClusteringColumnsRestrictions(hasQueriableIndex, selectsOnlyStaticColumns, selectsComplexColumn, forView); // Covers indexes on the first clustering column (among others). if (isKeyRange && hasQueriableClusteringColumnIndex) @@ -447,11 +447,11 @@ public final class StatementRestrictions * @param hasQueriableIndex true if some of the queried data are indexed, false otherwise * @param selectsOnlyStaticColumns true if the selected or modified columns are all statics, * false otherwise. - * @param selectACollection true if the query should return a collection column + * @param selectsComplexColumn true if the query should return a collection column */ private void processClusteringColumnsRestrictions(boolean hasQueriableIndex, boolean selectsOnlyStaticColumns, - boolean selectACollection, + boolean selectsComplexColumn, boolean forView) throws InvalidRequestException { checkFalse(!type.allowClusteringColumnSlices() && clusteringColumnsRestrictions.isSlice(), @@ -466,7 +466,7 @@ public final class StatementRestrictions } else { - checkFalse(clusteringColumnsRestrictions.isIN() && selectACollection, + checkFalse(clusteringColumnsRestrictions.isIN() && selectsComplexColumn, "Cannot restrict clustering columns by IN relations when a collection is selected by the query"); checkFalse(clusteringColumnsRestrictions.isContains() && !hasQueriableIndex, "Cannot restrict clustering columns by a CONTAINS relation without a secondary index"); diff --git a/src/java/org/apache/cassandra/cql3/selection/Selectable.java b/src/java/org/apache/cassandra/cql3/selection/Selectable.java index c3e0331ebd..faf3f2d41e 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selectable.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selectable.java @@ -277,23 +277,23 @@ public abstract class Selectable { Selector.Factory factory = selected.newSelectorFactory(cfm, defs); AbstractType type = factory.newInstance().getType(); - if (!(type instanceof UserType)) + if (!type.isUDT()) + { throw new InvalidRequestException( String.format("Invalid field selection: %s of type %s is not a user type", - selected, - type.asCQL3Type())); + selected, + type.asCQL3Type())); + } UserType ut = (UserType) type; - for (int i = 0; i < ut.size(); i++) + int fieldIndex = ((UserType) type).fieldPosition(field); + if (fieldIndex == -1) { - if (!ut.fieldName(i).equals(field.bytes)) - continue; - return FieldSelector.newFactory(ut, i, factory); + throw new InvalidRequestException(String.format("%s of type %s has no field %s", + selected, type.asCQL3Type(), field)); } - throw new InvalidRequestException(String.format("%s of type %s has no field %s", - selected, - type.asCQL3Type(), - field)); + + return FieldSelector.newFactory(ut, fieldIndex, factory); } public static class Raw implements Selectable.Raw diff --git a/src/java/org/apache/cassandra/cql3/selection/Selection.java b/src/java/org/apache/cassandra/cql3/selection/Selection.java index 4518f02950..e0e1bd875d 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selection.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selection.java @@ -112,14 +112,14 @@ public abstract class Selection } /** - * Checks if this selection contains a collection. + * Checks if this selection contains a complex column. * - * @return true if this selection contains a collection, false otherwise. + * @return true if this selection contains a multicell collection or UDT, false otherwise. */ - public boolean containsACollection() + public boolean containsAComplexColumn() { for (ColumnDefinition def : getColumns()) - if (def.type.isCollection() && def.type.isMultiCell()) + if (def.isComplex()) return true; return false; diff --git a/src/java/org/apache/cassandra/cql3/selection/Selector.java b/src/java/org/apache/cassandra/cql3/selection/Selector.java index 4515cd0df3..4c7e885a4b 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selector.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selector.java @@ -190,7 +190,7 @@ public abstract class Selector implements AssignmentTestable // We should ignore the fact that the output type is frozen in our comparison as functions do not support // frozen types for arguments AbstractType receiverType = receiver.type; - if (getType().isFrozenCollection()) + if (getType().isFreezable() && !getType().isMultiCell()) receiverType = receiverType.freeze(); if (getType().isReversed()) diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java index 8e51c262f7..5a8f3b6d8a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java @@ -166,11 +166,11 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement // If it's directly the type we've updated, then just use the new one. if (keyspace.equals(ut.keyspace) && toReplace.equals(ut.name)) - return updated; + return type.isMultiCell() ? updated : updated.freeze(); // Otherwise, check for nesting List> updatedTypes = updateTypes(ut.fieldTypes(), keyspace, toReplace, updated); - return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames()), updatedTypes); + return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames()), updatedTypes, type.isMultiCell()); } else if (type instanceof TupleType) { @@ -275,7 +275,7 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement newTypes.addAll(toUpdate.fieldTypes()); newTypes.add(addType); - return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes); + return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes, toUpdate.isMultiCell()); } private UserType doAlter(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException @@ -294,7 +294,7 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement List> newTypes = new ArrayList<>(toUpdate.fieldTypes()); newTypes.set(idx, type.prepare(keyspace()).getType()); - return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes); + return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes, toUpdate.isMultiCell()); } protected UserType makeUpdatedType(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException @@ -330,7 +330,7 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement newNames.set(idx, to.bytes); } - UserType updated = new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes); + UserType updated = new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes, toUpdate.isMultiCell()); CreateTypeStatement.checkForDuplicateNames(updated); return updated; } diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java index debb200901..8357c028dc 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/CreateTableStatement.java @@ -46,7 +46,7 @@ public class CreateTableStatement extends SchemaAlteringStatement private List> keyTypes; private List> clusteringTypes; - private final Map collections = new HashMap<>(); + private final Map multicellColumns = new HashMap<>(); private final List keyAliases = new ArrayList<>(); private final List columnAliases = new ArrayList<>(); @@ -223,10 +223,24 @@ public class CreateTableStatement extends SchemaAlteringStatement { ColumnIdentifier id = entry.getKey(); CQL3Type pt = entry.getValue().prepare(keyspace(), udts); - if (pt.isCollection() && ((CollectionType)pt.getType()).isMultiCell()) - stmt.collections.put(id.bytes, (CollectionType)pt.getType()); + if (pt.getType().isMultiCell()) + stmt.multicellColumns.put(id.bytes, pt.getType()); if (entry.getValue().isCounter()) stmt.hasCounters = true; + + // check for non-frozen UDTs or collections in a non-frozen UDT + if (pt.getType().isUDT() && pt.getType().isMultiCell()) + { + for (AbstractType innerType : ((UserType) pt.getType()).fieldTypes()) + { + if (innerType.isMultiCell()) + { + assert innerType.isCollection(); // shouldn't get this far with a nested non-frozen UDT + throw new InvalidRequestException("Non-frozen UDTs with nested non-frozen collections are not supported"); + } + } + } + stmt.columns.put(id, pt.getType()); // we'll remove what is not a column below } @@ -285,8 +299,8 @@ public class CreateTableStatement extends SchemaAlteringStatement // For COMPACT STORAGE, we reject any "feature" that we wouldn't be able to translate back to thrift. if (useCompactStorage) { - if (!stmt.collections.isEmpty()) - throw new InvalidRequestException("Non-frozen collection types are not supported with COMPACT STORAGE"); + if (!stmt.multicellColumns.isEmpty()) + throw new InvalidRequestException("Non-frozen collections and UDTs are not supported with COMPACT STORAGE"); if (!staticColumns.isEmpty()) throw new InvalidRequestException("Static columns are not supported in COMPACT STORAGE tables"); @@ -350,8 +364,13 @@ public class CreateTableStatement extends SchemaAlteringStatement AbstractType type = columns.get(t); if (type == null) throw new InvalidRequestException(String.format("Unknown definition %s referenced in PRIMARY KEY", t)); - if (type.isCollection() && type.isMultiCell()) - throw new InvalidRequestException(String.format("Invalid collection type for PRIMARY KEY component %s", t)); + if (type.isMultiCell()) + { + if (type.isCollection()) + throw new InvalidRequestException(String.format("Invalid non-frozen collection type for PRIMARY KEY component %s", t)); + else + throw new InvalidRequestException(String.format("Invalid non-frozen user-defined type for PRIMARY KEY component %s", t)); + } columns.remove(t); Boolean isReversed = properties.definedOrdering.get(t); diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java index f62b9eac61..e134594d4d 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java @@ -74,8 +74,12 @@ public class CreateTypeStatement extends SchemaAlteringStatement throw new InvalidRequestException(String.format("A user type of name %s already exists", name)); for (CQL3Type.Raw type : columnTypes) + { if (type.isCounter()) throw new InvalidRequestException("A user type cannot contain counters"); + if (type.isUDT() && !type.isFrozen()) + throw new InvalidRequestException("A user type cannot contain non-frozen UDTs"); + } } public static void checkForDuplicateNames(UserType type) throws InvalidRequestException @@ -109,7 +113,7 @@ public class CreateTypeStatement extends SchemaAlteringStatement for (CQL3Type.Raw type : columnTypes) types.add(type.prepare(keyspace()).getType()); - return new UserType(name.getKeyspace(), name.getUserTypeName(), names, types); + return new UserType(name.getKeyspace(), name.getUserTypeName(), names, types, true); } public Event.SchemaChange announceMigration(boolean isLocalOnly) throws InvalidRequestException, ConfigurationException diff --git a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java index daeecfe4bb..accbe0c859 100644 --- a/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/DeleteStatement.java @@ -147,7 +147,7 @@ public class DeleteStatement extends ModificationStatement // list. However, we support having the value name for coherence with the static/sparse case checkFalse(def.isPrimaryKeyColumn(), "Invalid identifier %s for deletion (should not be a PRIMARY KEY part)", def.name); - Operation op = deletion.prepare(cfm.ksName, def); + Operation op = deletion.prepare(cfm.ksName, def, cfm); op.collectMarkerSpecification(boundNames); operations.add(op); } diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 06ff5d4fca..7243daac2c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -831,7 +831,7 @@ public abstract class ModificationStatement implements CQLStatement ColumnDefinition def = metadata.getColumnDefinition(id); checkNotNull(metadata.getColumnDefinition(id), "Unknown identifier %s in IF conditions", id); - ColumnCondition condition = entry.right.prepare(keyspace(), def); + ColumnCondition condition = entry.right.prepare(keyspace(), def, metadata); condition.collectMarkerSpecification(boundNames); checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY column '%s' cannot have IF conditions", id); diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index b164e618c5..9895d67e14 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -39,6 +39,7 @@ import org.apache.cassandra.db.filter.*; import org.apache.cassandra.db.marshal.CollectionType; import org.apache.cassandra.db.marshal.CompositeType; import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.ComplexColumnData; import org.apache.cassandra.db.rows.Row; @@ -759,13 +760,14 @@ public class SelectStatement implements CQLStatement { if (def.isComplex()) { - // Collections are the only complex types we have so far - assert def.type.isCollection() && def.type.isMultiCell(); + assert def.type.isMultiCell(); ComplexColumnData complexData = row.getComplexColumnData(def); if (complexData == null) - result.add((ByteBuffer)null); + result.add(null); + else if (def.type.isCollection()) + result.add(((CollectionType) def.type).serializeForNativeProtocol(complexData.iterator(), protocolVersion)); else - result.add(((CollectionType)def.type).serializeForNativeProtocol(def, complexData.iterator(), protocolVersion)); + result.add(((UserType) def.type).serializeForNativeProtocol(complexData.iterator(), protocolVersion)); } else { @@ -883,7 +885,7 @@ public class SelectStatement implements CQLStatement whereClause, boundNames, selection.containsOnlyStaticColumns(), - selection.containsACollection(), + selection.containsAComplexColumn(), parameters.allowFiltering, forView); } diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java index 6f872d4644..fcc0ca6f90 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdateStatement.java @@ -170,7 +170,7 @@ public class UpdateStatement extends ModificationStatement } else { - Operation operation = new Operation.SetValue(value).prepare(keyspace(), def); + Operation operation = new Operation.SetValue(value).prepare(cfm, def); operation.collectMarkerSpecification(boundNames); operations.add(operation); } @@ -239,7 +239,7 @@ public class UpdateStatement extends ModificationStatement } else { - Operation operation = new Operation.SetValue(raw).prepare(keyspace(), def); + Operation operation = new Operation.SetValue(raw).prepare(cfm, def); operation.collectMarkerSpecification(boundNames); operations.add(operation); } @@ -308,7 +308,7 @@ public class UpdateStatement extends ModificationStatement checkFalse(def.isPrimaryKeyColumn(), "PRIMARY KEY part %s found in SET part", def.name); - Operation operation = entry.right.prepare(keyspace(), def); + Operation operation = entry.right.prepare(cfm, def); operation.collectMarkerSpecification(boundNames); operations.add(operation); } diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index 331f1a4ba1..1b94a74441 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -312,11 +312,21 @@ public abstract class AbstractType implements Comparator return false; } + public boolean isUDT() + { + return false; + } + public boolean isMultiCell() { return false; } + public boolean isFreezable() + { + return false; + } + public AbstractType freeze() { return this; @@ -418,6 +428,17 @@ public abstract class AbstractType implements Comparator return getClass().getName(); } + /** + * Checks to see if two types are equal when ignoring or not ignoring differences in being frozen, depending on + * the value of the ignoreFreezing parameter. + * @param other type to compare + * @param ignoreFreezing if true, differences in the types being frozen will be ignored + */ + public boolean equals(Object other, boolean ignoreFreezing) + { + return this.equals(other); + } + public void checkComparable() { switch (comparisonType) diff --git a/src/java/org/apache/cassandra/db/marshal/CollectionType.java b/src/java/org/apache/cassandra/db/marshal/CollectionType.java index d65e3a69e8..2f5cbb69f9 100644 --- a/src/java/org/apache/cassandra/db/marshal/CollectionType.java +++ b/src/java/org/apache/cassandra/db/marshal/CollectionType.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.List; import java.util.Iterator; -import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.cql3.ColumnSpecification; import org.apache.cassandra.cql3.Lists; @@ -133,13 +132,19 @@ public abstract class CollectionType extends AbstractType return kind == Kind.MAP; } + @Override + public boolean isFreezable() + { + return true; + } + // Overrided by maps protected int collectionSize(List values) { return values.size(); } - public ByteBuffer serializeForNativeProtocol(ColumnDefinition def, Iterator cells, int version) + public ByteBuffer serializeForNativeProtocol(Iterator cells, int version) { assert isMultiCell(); List values = serializedValues(cells); @@ -203,6 +208,27 @@ public abstract class CollectionType extends AbstractType return new CQL3Type.Collection(this); } + @Override + public boolean equals(Object o, boolean ignoreFreezing) + { + if (this == o) + return true; + + if (!(o instanceof CollectionType)) + return false; + + CollectionType other = (CollectionType)o; + + if (kind != other.kind) + return false; + + if (!ignoreFreezing && isMultiCell() != other.isMultiCell()) + return false; + + return nameComparator().equals(other.nameComparator(), ignoreFreezing) && + valueComparator().equals(other.valueComparator(), ignoreFreezing); + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java index 43cf52c729..eaf6653d44 100644 --- a/src/java/org/apache/cassandra/db/marshal/TupleType.java +++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java @@ -23,11 +23,13 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; +import java.util.stream.Collectors; import com.google.common.base.Objects; import org.apache.cassandra.cql3.*; import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.*; import org.apache.cassandra.utils.ByteBufferUtil; @@ -50,11 +52,17 @@ public class TupleType extends AbstractType protected final List> types; public TupleType(List> types) + { + this(types, true); + } + + protected TupleType(List> types, boolean freezeInner) { super(ComparisonType.CUSTOM); - for (int i = 0; i < types.size(); i++) - types.set(i, types.get(i).freeze()); - this.types = types; + if (freezeInner) + this.types = types.stream().map(AbstractType::freeze).collect(Collectors.toList()); + else + this.types = types; } public static TupleType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException @@ -118,11 +126,22 @@ public class TupleType extends AbstractType return cmp; } - if (bb1.remaining() == 0) - return bb2.remaining() == 0 ? 0 : -1; + // handle trailing nulls + while (bb1.remaining() > 0) + { + int size = bb1.getInt(); + if (size > 0) // non-null + return 1; + } - // bb1.remaining() > 0 && bb2.remaining() == 0 - return 1; + while (bb2.remaining() > 0) + { + int size = bb2.getInt(); + if (size > 0) // non-null + return -1; + } + + return 0; } @Override @@ -171,6 +190,15 @@ public class TupleType extends AbstractType int size = input.getInt(); components[i] = size < 0 ? null : ByteBufferUtil.readBytes(input, size); } + + // error out if we got more values in the tuple/UDT than we expected + if (input.hasRemaining()) + { + throw new InvalidRequestException(String.format( + "Expected %s %s for %s column, but got more", + size(), size() == 1 ? "value" : "values", this.asCQL3Type())); + } + return components; } @@ -200,6 +228,9 @@ public class TupleType extends AbstractType @Override public String getString(ByteBuffer value) { + if (value == null) + return "null"; + StringBuilder sb = new StringBuilder(); ByteBuffer input = value.duplicate(); for (int i = 0; i < size(); i++) diff --git a/src/java/org/apache/cassandra/db/marshal/TypeParser.java b/src/java/org/apache/cassandra/db/marshal/TypeParser.java index 35d15abd0a..ba8ad1322e 100644 --- a/src/java/org/apache/cassandra/db/marshal/TypeParser.java +++ b/src/java/org/apache/cassandra/db/marshal/TypeParser.java @@ -538,7 +538,8 @@ public class TypeParser return sb.toString(); } - public static String stringifyUserTypeParameters(String keysace, ByteBuffer typeName, List columnNames, List> columnTypes) + public static String stringifyUserTypeParameters(String keysace, ByteBuffer typeName, List columnNames, + List> columnTypes, boolean ignoreFreezing) { StringBuilder sb = new StringBuilder(); sb.append('(').append(keysace).append(",").append(ByteBufferUtil.bytesToHex(typeName)); @@ -547,8 +548,7 @@ public class TypeParser { sb.append(','); sb.append(ByteBufferUtil.bytesToHex(columnNames.get(i))).append(":"); - // omit FrozenType(...) from fields because it is currently implicit - sb.append(columnTypes.get(i).toString(true)); + sb.append(columnTypes.get(i).toString(ignoreFreezing)); } sb.append(')'); return sb.toString(); diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index e766190372..72ed895285 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -25,11 +25,15 @@ import java.util.*; import com.google.common.base.Objects; import org.apache.cassandra.cql3.*; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.serializers.*; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A user defined type. @@ -38,19 +42,24 @@ import org.apache.cassandra.utils.Pair; */ public class UserType extends TupleType { + private static final Logger logger = LoggerFactory.getLogger(UserType.class); + public final String keyspace; public final ByteBuffer name; private final List fieldNames; private final List stringFieldNames; + private final boolean isMultiCell; - public UserType(String keyspace, ByteBuffer name, List fieldNames, List> fieldTypes) + public UserType(String keyspace, ByteBuffer name, List fieldNames, List> fieldTypes, boolean isMultiCell) { - super(fieldTypes); + super(fieldTypes, false); assert fieldNames.size() == fieldTypes.size(); this.keyspace = keyspace; this.name = name; this.fieldNames = fieldNames; this.stringFieldNames = new ArrayList<>(fieldNames.size()); + this.isMultiCell = isMultiCell; + for (ByteBuffer fieldName : fieldNames) { try @@ -74,9 +83,28 @@ public class UserType extends TupleType for (Pair p : params.right) { columnNames.add(p.left); - columnTypes.add(p.right.freeze()); + columnTypes.add(p.right); } - return new UserType(keyspace, name, columnNames, columnTypes); + + return new UserType(keyspace, name, columnNames, columnTypes, true); + } + + @Override + public boolean isUDT() + { + return true; + } + + @Override + public boolean isMultiCell() + { + return isMultiCell; + } + + @Override + public boolean isFreezable() + { + return true; } public AbstractType fieldType(int i) @@ -109,6 +137,55 @@ public class UserType extends TupleType return UTF8Type.instance.compose(name); } + public short fieldPosition(ColumnIdentifier field) + { + return fieldPosition(field.bytes); + } + + public short fieldPosition(ByteBuffer fieldName) + { + for (short i = 0; i < fieldNames.size(); i++) + if (fieldName.equals(fieldNames.get(i))) + return i; + return -1; + } + + public CellPath cellPathForField(ByteBuffer fieldName) + { + // we use the field position instead of the field name to allow for field renaming in ALTER TYPE statements + return CellPath.create(ByteBufferUtil.bytes(fieldPosition(fieldName))); + } + + public ShortType nameComparator() + { + return ShortType.instance; + } + + public ByteBuffer serializeForNativeProtocol(Iterator cells, int protocolVersion) + { + assert isMultiCell; + + ByteBuffer[] components = new ByteBuffer[size()]; + short fieldPosition = 0; + while (cells.hasNext()) + { + Cell cell = cells.next(); + + // handle null fields that aren't at the end + short fieldPositionOfCell = ByteBufferUtil.toShort(cell.path().get(0)); + while (fieldPosition < fieldPositionOfCell) + components[fieldPosition++] = null; + + components[fieldPosition++] = cell.value(); + } + + // append trailing nulls for missing cells + while (fieldPosition < size()) + components[fieldPosition++] = null; + + return TupleType.buildValue(components); + } + // Note: the only reason we override this is to provide nicer error message, but since that's not that much code... @Override public void validate(ByteBuffer bytes) throws MarshalException @@ -216,20 +293,80 @@ public class UserType extends TupleType return sb.append("}").toString(); } + @Override + public UserType freeze() + { + if (isMultiCell) + return new UserType(keyspace, name, fieldNames, fieldTypes(), false); + else + return this; + } + @Override public int hashCode() { - return Objects.hashCode(keyspace, name, fieldNames, types); + return Objects.hashCode(keyspace, name, fieldNames, types, isMultiCell); + } + + @Override + public boolean isValueCompatibleWith(AbstractType previous) + { + if (this == previous) + return true; + + if (!(previous instanceof UserType)) + return false; + + UserType other = (UserType) previous; + if (isMultiCell != other.isMultiCell()) + return false; + + if (!keyspace.equals(other.keyspace)) + return false; + + Iterator> thisTypeIter = types.iterator(); + Iterator> previousTypeIter = other.types.iterator(); + while (thisTypeIter.hasNext() && previousTypeIter.hasNext()) + { + if (!thisTypeIter.next().isCompatibleWith(previousTypeIter.next())) + return false; + } + + // it's okay for the new type to have additional fields, but not for the old type to have additional fields + return !previousTypeIter.hasNext(); } @Override public boolean equals(Object o) + { + return o instanceof UserType && equals(o, false); + } + + @Override + public boolean equals(Object o, boolean ignoreFreezing) { if(!(o instanceof UserType)) return false; UserType that = (UserType)o; - return keyspace.equals(that.keyspace) && name.equals(that.name) && fieldNames.equals(that.fieldNames) && types.equals(that.types); + + if (!keyspace.equals(that.keyspace) || !name.equals(that.name) || !fieldNames.equals(that.fieldNames)) + return false; + + if (!ignoreFreezing && isMultiCell != that.isMultiCell) + return false; + + if (this.types.size() != that.types.size()) + return false; + + Iterator> otherTypeIter = that.types.iterator(); + for (AbstractType type : types) + { + if (!type.equals(otherTypeIter.next(), ignoreFreezing)) + return false; + } + + return true; } @Override @@ -248,6 +385,21 @@ public class UserType extends TupleType @Override public String toString() { - return getClass().getName() + TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types); + return this.toString(false); + } + + @Override + public String toString(boolean ignoreFreezing) + { + boolean includeFrozenType = !ignoreFreezing && !isMultiCell(); + + StringBuilder sb = new StringBuilder(); + if (includeFrozenType) + sb.append(FrozenType.class.getName()).append("("); + sb.append(getClass().getName()); + sb.append(TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types, ignoreFreezing || !isMultiCell)); + if (includeFrozenType) + sb.append(")"); + return sb.toString(); } } diff --git a/src/java/org/apache/cassandra/db/rows/CellPath.java b/src/java/org/apache/cassandra/db/rows/CellPath.java index 68e3c2bc4b..e2b362c925 100644 --- a/src/java/org/apache/cassandra/db/rows/CellPath.java +++ b/src/java/org/apache/cassandra/db/rows/CellPath.java @@ -22,6 +22,7 @@ import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.Objects; +import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.utils.ByteBufferUtil; @@ -39,11 +40,11 @@ public abstract class CellPath public abstract int size(); public abstract ByteBuffer get(int i); - // The only complex we currently have are collections that have only one value. + // The only complex paths we currently have are collections and UDTs, which both have a depth of one public static CellPath create(ByteBuffer value) { assert value != null; - return new CollectionCellPath(value); + return new SingleItemCellPath(value); } public int dataSize() @@ -98,13 +99,13 @@ public abstract class CellPath public void skip(DataInputPlus in) throws IOException; } - private static class CollectionCellPath extends CellPath + private static class SingleItemCellPath extends CellPath { - private static final long EMPTY_SIZE = ObjectSizes.measure(new CollectionCellPath(ByteBufferUtil.EMPTY_BYTE_BUFFER)); + private static final long EMPTY_SIZE = ObjectSizes.measure(new SingleItemCellPath(ByteBufferUtil.EMPTY_BYTE_BUFFER)); protected final ByteBuffer value; - private CollectionCellPath(ByteBuffer value) + private SingleItemCellPath(ByteBuffer value) { this.value = value; } @@ -122,7 +123,7 @@ public abstract class CellPath public CellPath copy(AbstractAllocator allocator) { - return new CollectionCellPath(allocator.clone(value)); + return new SingleItemCellPath(allocator.clone(value)); } public long unsharedHeapSizeExcludingData() diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java index ac137e735a..f4678b7644 100644 --- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java +++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java @@ -251,7 +251,6 @@ public class ComplexColumnData extends ColumnData implements Iterable public void addCell(Cell cell) { - assert cell.column().equals(column); builder.add(cell); } diff --git a/src/java/org/apache/cassandra/schema/Functions.java b/src/java/org/apache/cassandra/schema/Functions.java index c65f58db34..a936d81842 100644 --- a/src/java/org/apache/cassandra/schema/Functions.java +++ b/src/java/org/apache/cassandra/schema/Functions.java @@ -131,7 +131,7 @@ public final class Functions implements Iterable */ public static boolean typesMatch(AbstractType t1, AbstractType t2) { - return t1.asCQL3Type().toString().equals(t2.asCQL3Type().toString()); + return t1.freeze().asCQL3Type().toString().equals(t2.freeze().asCQL3Type().toString()); } public static boolean typesMatch(List> t1, List> t2) diff --git a/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java b/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java index 212676dae8..9d277d6e7a 100644 --- a/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java +++ b/src/java/org/apache/cassandra/schema/LegacySchemaMigrator.java @@ -827,7 +827,7 @@ public final class LegacySchemaMigrator .map(LegacySchemaMigrator::parseType) .collect(Collectors.toList()); - return new UserType(keyspaceName, bytes(typeName), names, types); + return new UserType(keyspaceName, bytes(typeName), names, types, true); } /* diff --git a/src/java/org/apache/cassandra/schema/Types.java b/src/java/org/apache/cassandra/schema/Types.java index 1b713646b9..00801b5719 100644 --- a/src/java/org/apache/cassandra/schema/Types.java +++ b/src/java/org/apache/cassandra/schema/Types.java @@ -22,11 +22,7 @@ import java.util.*; import javax.annotation.Nullable; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.MapDifference; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; +import com.google.common.collect.*; import org.apache.cassandra.cql3.CQL3Type; import org.apache.cassandra.db.marshal.AbstractType; @@ -139,7 +135,30 @@ public final class Types implements Iterable @Override public boolean equals(Object o) { - return this == o || (o instanceof Types && types.equals(((Types) o).types)); + if (this == o) + return true; + + if (!(o instanceof Types)) + return false; + + Types other = (Types) o; + + if (types.size() != other.types.size()) + return false; + + Iterator> thisIter = this.types.entrySet().iterator(); + Iterator> otherIter = other.types.entrySet().iterator(); + while (thisIter.hasNext()) + { + Map.Entry thisNext = thisIter.next(); + Map.Entry otherNext = otherIter.next(); + if (!thisNext.getKey().equals(otherNext.getKey())) + return false; + + if (!thisNext.getValue().equals(otherNext.getValue(), true)) // ignore freezing + return false; + } + return true; } @Override @@ -156,7 +175,7 @@ public final class Types implements Iterable public static final class Builder { - final ImmutableMap.Builder types = ImmutableMap.builder(); + final ImmutableSortedMap.Builder types = ImmutableSortedMap.naturalOrder(); private Builder() { @@ -169,6 +188,7 @@ public final class Types implements Iterable public Builder add(UserType type) { + assert type.isMultiCell(); types.put(type.name, type); return this; } @@ -293,7 +313,7 @@ public final class Types implements Iterable .map(t -> t.prepareInternal(keyspace, types).getType()) .collect(toList()); - return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes); + return new UserType(keyspace, bytes(name), preparedFieldNames, preparedFieldTypes, true); } @Override diff --git a/src/java/org/apache/cassandra/service/MigrationManager.java b/src/java/org/apache/cassandra/service/MigrationManager.java index ba8a5fb91d..571748b96f 100644 --- a/src/java/org/apache/cassandra/service/MigrationManager.java +++ b/src/java/org/apache/cassandra/service/MigrationManager.java @@ -431,6 +431,7 @@ public class MigrationManager public static void announceTypeUpdate(UserType updatedType, boolean announceLocally) { + logger.info(String.format("Update type '%s.%s' to %s", updatedType.keyspace, updatedType.getNameAsString(), updatedType)); announceNewType(updatedType, announceLocally); } diff --git a/src/java/org/apache/cassandra/transport/DataType.java b/src/java/org/apache/cassandra/transport/DataType.java index e3eaf3247b..acaa9a37b9 100644 --- a/src/java/org/apache/cassandra/transport/DataType.java +++ b/src/java/org/apache/cassandra/transport/DataType.java @@ -116,7 +116,7 @@ public enum DataType implements OptionCodec.Codecable fieldNames.add(UTF8Type.instance.decompose(CBUtil.readString(cb))); fieldTypes.add(DataType.toType(codec.decodeOne(cb, version))); } - return new UserType(ks, name, fieldNames, fieldTypes); + return new UserType(ks, name, fieldNames, fieldTypes, true); case TUPLE: n = cb.readUnsignedShort(); List> types = new ArrayList<>(n); diff --git a/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java b/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java index 02ed1a8d2a..73e0fcae34 100644 --- a/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java +++ b/test/unit/org/apache/cassandra/cql3/CQL3TypeLiteralTest.java @@ -636,7 +636,7 @@ public class CQL3TypeLiteralTest names.add(UTF8Type.instance.fromString('f' + randLetters(i))); types.add(randomNestedType(level)); } - return new UserType("ks", UTF8Type.instance.fromString("u" + randInt(1000000)), names, types); + return new UserType("ks", UTF8Type.instance.fromString("u" + randInt(1000000)), names, types, true); } // diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java index 8334321ba0..1b36c03f00 100644 --- a/test/unit/org/apache/cassandra/cql3/CQLTester.java +++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java @@ -139,6 +139,11 @@ public abstract class CQLTester private boolean usePrepared = USE_PREPARED_VALUES; private static boolean reusePrepared = REUSE_PREPARED; + protected boolean usePrepared() + { + return usePrepared; + } + public static void prepareServer() { if (isServerPrepared) @@ -1117,6 +1122,24 @@ public abstract class CQLTester e.getMessage().contains(text)); } + @FunctionalInterface + public interface CheckedFunction { + void apply() throws Throwable; + } + + /** + * Runs the given function before and after a flush of sstables. This is useful for checking that behavior is + * the same whether data is in memtables or sstables. + * @param runnable + * @throws Throwable + */ + public void beforeAndAfterFlush(CheckedFunction runnable) throws Throwable + { + runnable.apply(); + flush(); + runnable.apply(); + } + private static String replaceValues(String query, Object[] values) { StringBuilder sb = new StringBuilder(); @@ -1327,7 +1350,7 @@ public abstract class CQLTester if (value instanceof ByteBuffer) return (ByteBuffer)value; - return type.decompose(value); + return type.decompose(serializeTuples(value)); } private static String formatValue(ByteBuffer bb, AbstractType type) @@ -1354,7 +1377,19 @@ public abstract class CQLTester protected Object userType(Object... values) { - return new TupleValue(values).toByteBuffer(); + if (values.length % 2 != 0) + throw new IllegalArgumentException("userType() requires an even number of arguments"); + + String[] fieldNames = new String[values.length / 2]; + Object[] fieldValues = new Object[values.length / 2]; + int fieldNum = 0; + for (int i = 0; i < values.length; i += 2) + { + fieldNames[fieldNum] = (String) values[i]; + fieldValues[fieldNum] = values[i + 1]; + fieldNum++; + } + return new UserTypeValue(fieldNames, fieldValues); } protected Object list(Object...values) @@ -1468,7 +1503,7 @@ public abstract class CQLTester private static class TupleValue { - private final Object[] values; + protected final Object[] values; TupleValue(Object[] values) { @@ -1502,4 +1537,43 @@ public abstract class CQLTester return "TupleValue" + toCQLString(); } } + + private static class UserTypeValue extends TupleValue + { + private final String[] fieldNames; + + UserTypeValue(String[] fieldNames, Object[] fieldValues) + { + super(fieldValues); + this.fieldNames = fieldNames; + } + + @Override + public String toCQLString() + { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + boolean haveEntry = false; + for (int i = 0; i < values.length; i++) + { + if (values[i] != null) + { + if (haveEntry) + sb.append(", "); + sb.append(ColumnIdentifier.maybeQuote(fieldNames[i])); + sb.append(": "); + sb.append(formatForCQL(values[i])); + haveEntry = true; + } + } + assert haveEntry; + sb.append("}"); + return sb.toString(); + } + + public String toString() + { + return "UserTypeValue" + toCQLString(); + } + } } diff --git a/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java b/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java index 71524c5821..989c524854 100644 --- a/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/ColumnConditionTest.java @@ -186,7 +186,7 @@ public class ColumnConditionTest ColumnDefinition definition = ColumnDefinition.regularDef("ks", "cf", "c", ListType.getInstance(Int32Type.instance, true)); // EQ - ColumnCondition condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.EQ); + ColumnCondition condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.EQ); ColumnCondition.CollectionBound bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(listAppliesTo(bound, list(ONE), list(ONE))); assertTrue(listAppliesTo(bound, list(), list())); @@ -202,7 +202,7 @@ public class ColumnConditionTest assertTrue(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // NEQ - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.NEQ); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.NEQ); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(listAppliesTo(bound, list(ONE), list(ONE))); assertFalse(listAppliesTo(bound, list(), list())); @@ -218,7 +218,7 @@ public class ColumnConditionTest assertFalse(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // LT - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LT); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LT); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(listAppliesTo(bound, list(ONE), list(ONE))); assertFalse(listAppliesTo(bound, list(), list())); @@ -234,7 +234,7 @@ public class ColumnConditionTest assertFalse(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // LTE - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LTE); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LTE); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(listAppliesTo(bound, list(ONE), list(ONE))); assertTrue(listAppliesTo(bound, list(), list())); @@ -250,7 +250,7 @@ public class ColumnConditionTest assertTrue(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // GT - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GT); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GT); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(listAppliesTo(bound, list(ONE), list(ONE))); assertFalse(listAppliesTo(bound, list(), list())); @@ -266,7 +266,7 @@ public class ColumnConditionTest assertFalse(listAppliesTo(bound, list(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // GTE - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GTE); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GTE); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(listAppliesTo(bound, list(ONE), list(ONE))); assertTrue(listAppliesTo(bound, list(), list())); @@ -315,7 +315,7 @@ public class ColumnConditionTest ColumnDefinition definition = ColumnDefinition.regularDef("ks", "cf", "c", ListType.getInstance(Int32Type.instance, true)); // EQ - ColumnCondition condition = ColumnCondition.condition(definition, null, new Sets.Value(set(ONE)), Operator.EQ); + ColumnCondition condition = ColumnCondition.condition(definition, new Sets.Value(set(ONE)), Operator.EQ); ColumnCondition.CollectionBound bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(setAppliesTo(bound, set(ONE), list(ONE))); assertTrue(setAppliesTo(bound, set(), list())); @@ -331,7 +331,7 @@ public class ColumnConditionTest assertTrue(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // NEQ - condition = ColumnCondition.condition(definition, null, new Sets.Value(set(ONE)), Operator.NEQ); + condition = ColumnCondition.condition(definition, new Sets.Value(set(ONE)), Operator.NEQ); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(setAppliesTo(bound, set(ONE), list(ONE))); assertFalse(setAppliesTo(bound, set(), list())); @@ -347,7 +347,7 @@ public class ColumnConditionTest assertFalse(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // LT - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LT); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LT); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(setAppliesTo(bound, set(ONE), list(ONE))); assertFalse(setAppliesTo(bound, set(), list())); @@ -363,7 +363,7 @@ public class ColumnConditionTest assertFalse(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // LTE - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.LTE); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.LTE); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(setAppliesTo(bound, set(ONE), list(ONE))); assertTrue(setAppliesTo(bound, set(), list())); @@ -379,7 +379,7 @@ public class ColumnConditionTest assertTrue(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // GT - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GT); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GT); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(setAppliesTo(bound, set(ONE), list(ONE))); assertFalse(setAppliesTo(bound, set(), list())); @@ -395,7 +395,7 @@ public class ColumnConditionTest assertFalse(setAppliesTo(bound, set(ByteBufferUtil.EMPTY_BYTE_BUFFER), list(ByteBufferUtil.EMPTY_BYTE_BUFFER))); // GTE - condition = ColumnCondition.condition(definition, null, new Lists.Value(Arrays.asList(ONE)), Operator.GTE); + condition = ColumnCondition.condition(definition, new Lists.Value(Arrays.asList(ONE)), Operator.GTE); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(setAppliesTo(bound, set(ONE), list(ONE))); assertTrue(setAppliesTo(bound, set(), list())); @@ -448,7 +448,7 @@ public class ColumnConditionTest Maps.Value placeholder = new Maps.Value(placeholderMap); // EQ - ColumnCondition condition = ColumnCondition.condition(definition, null, placeholder, Operator.EQ); + ColumnCondition condition = ColumnCondition.condition(definition, placeholder, Operator.EQ); ColumnCondition.CollectionBound bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE))); @@ -470,7 +470,7 @@ public class ColumnConditionTest assertTrue(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER))); // NEQ - condition = ColumnCondition.condition(definition, null, placeholder, Operator.NEQ); + condition = ColumnCondition.condition(definition, placeholder, Operator.NEQ); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE))); @@ -492,7 +492,7 @@ public class ColumnConditionTest assertFalse(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER))); // LT - condition = ColumnCondition.condition(definition, null, placeholder, Operator.LT); + condition = ColumnCondition.condition(definition, placeholder, Operator.LT); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE))); @@ -514,7 +514,7 @@ public class ColumnConditionTest assertFalse(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER))); // LTE - condition = ColumnCondition.condition(definition, null, placeholder, Operator.LTE); + condition = ColumnCondition.condition(definition, placeholder, Operator.LTE); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE))); @@ -536,7 +536,7 @@ public class ColumnConditionTest assertTrue(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER))); // GT - condition = ColumnCondition.condition(definition, null, placeholder, Operator.GT); + condition = ColumnCondition.condition(definition, placeholder, Operator.GT); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertFalse(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE))); @@ -558,7 +558,7 @@ public class ColumnConditionTest assertFalse(mapAppliesTo(bound, map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER), map(ONE, ByteBufferUtil.EMPTY_BYTE_BUFFER))); // GTE - condition = ColumnCondition.condition(definition, null, placeholder, Operator.GTE); + condition = ColumnCondition.condition(definition, placeholder, Operator.GTE); bound = (ColumnCondition.CollectionBound) condition.bind(QueryOptions.DEFAULT); assertTrue(mapAppliesTo(bound, map(ONE, ONE), map(ONE, ONE))); diff --git a/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java b/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java index e562aac440..3c83da7f85 100644 --- a/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java +++ b/test/unit/org/apache/cassandra/cql3/selection/SelectionColumnMappingTest.java @@ -48,7 +48,7 @@ public class SelectionColumnMappingTest extends CQLTester " v1 int," + " v2 ascii," + " v3 frozen<" + typeName + ">)"); - userType = Schema.instance.getKSMetaData(KEYSPACE).types.get(ByteBufferUtil.bytes(typeName)).get(); + userType = Schema.instance.getKSMetaData(KEYSPACE).types.get(ByteBufferUtil.bytes(typeName)).get().freeze(); functionName = createFunction(KEYSPACE, "int, ascii", "CREATE FUNCTION %s (i int, a ascii) " + "CALLED ON NULL INPUT " + diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java index d9df2062c4..5501561e2c 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/UserTypesTest.java @@ -73,32 +73,86 @@ public class UserTypesTest extends CQLTester execute("INSERT INTO %s(k, v) VALUES (?, {x:?})", 1, -104.99251); execute("UPDATE %s SET b = ? WHERE k = ?", true, 1); - assertRows(execute("SELECT v.x FROM %s WHERE k = ? AND v = {x:?}", 1, -104.99251), - row(-104.99251) - ); - - flush(); - - assertRows(execute("SELECT v.x FROM %s WHERE k = ? AND v = {x:?}", 1, -104.99251), - row(-104.99251) + beforeAndAfterFlush(() -> + assertRows(execute("SELECT v.x FROM %s WHERE k = ? AND v = {x:?}", 1, -104.99251), + row(-104.99251) + ) ); } @Test - public void testCreateInvalidTablesWithUDT() throws Throwable + public void testInvalidUDTStatements() throws Throwable { - String myType = createType("CREATE TYPE %s (f int)"); + String typename = createType("CREATE TYPE %s (a int)"); + String myType = KEYSPACE + '.' + typename; - // Using a UDT without frozen shouldn't work - assertInvalidMessage("Non-frozen User-Defined types are not supported, please use frozen<>", - "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v " + KEYSPACE + '.' + myType + ")"); + // non-frozen UDTs in a table PK + assertInvalidMessage("Invalid non-frozen user-defined type for PRIMARY KEY component k", + "CREATE TABLE " + KEYSPACE + ".wrong (k " + myType + " PRIMARY KEY , v int)"); + assertInvalidMessage("Invalid non-frozen user-defined type for PRIMARY KEY component k2", + "CREATE TABLE " + KEYSPACE + ".wrong (k1 int, k2 " + myType + ", v int, PRIMARY KEY (k1, k2))"); + // non-frozen UDTs in a collection + assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: list<" + myType + ">", + "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v list<" + myType + ">)"); + assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: set<" + myType + ">", + "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v set<" + myType + ">)"); + assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: map<" + myType + ", int>", + "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v map<" + myType + ", int>)"); + assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: map", + "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v map)"); + + // non-frozen UDT in a collection (as part of a UDT definition) + assertInvalidMessage("Non-frozen UDTs are not allowed inside collections: list<" + myType + ">", + "CREATE TYPE " + KEYSPACE + ".wrong (a int, b list<" + myType + ">)"); + + // non-frozen UDT in a UDT + assertInvalidMessage("A user type cannot contain non-frozen UDTs", + "CREATE TYPE " + KEYSPACE + ".wrong (a int, b " + myType + ")"); + + // referencing a UDT in another keyspace assertInvalidMessage("Statement on keyspace " + KEYSPACE + " cannot refer to a user type in keyspace otherkeyspace;" + " user types can only be used in the keyspace they are defined in", "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v frozen)"); + // referencing an unknown UDT assertInvalidMessage("Unknown type " + KEYSPACE + ".unknowntype", "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v frozen<" + KEYSPACE + '.' + "unknownType>)"); + + // bad deletions on frozen UDTs + createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<" + myType + ">, c int)"); + assertInvalidMessage("Frozen UDT column b does not support field deletion", "DELETE b.a FROM %s WHERE a = 0"); + assertInvalidMessage("Invalid field deletion operation for non-UDT column c", "DELETE c.a FROM %s WHERE a = 0"); + + // bad updates on frozen UDTs + assertInvalidMessage("Invalid operation (b.a = 0) for frozen UDT column b", "UPDATE %s SET b.a = 0 WHERE a = 0"); + assertInvalidMessage("Invalid operation (c.a = 0) for non-UDT column c", "UPDATE %s SET c.a = 0 WHERE a = 0"); + + // bad deletions on non-frozen UDTs + createTable("CREATE TABLE %s (a int PRIMARY KEY, b " + myType + ", c int)"); + assertInvalidMessage("UDT column b does not have a field named foo", "DELETE b.foo FROM %s WHERE a = 0"); + + // bad updates on non-frozen UDTs + assertInvalidMessage("UDT column b does not have a field named foo", "UPDATE %s SET b.foo = 0 WHERE a = 0"); + + // bad insert on non-frozen UDTs + assertInvalidMessage("Unknown field 'foo' in value of user defined type", "INSERT INTO %s (a, b, c) VALUES (0, {a: 0, foo: 0}, 0)"); + if (usePrepared()) + { + assertInvalidMessage("Expected 1 value for " + typename + " column, but got more", + "INSERT INTO %s (a, b, c) VALUES (0, ?, 0)", userType("a", 0, "foo", 0)); + } + else + { + assertInvalidMessage("Unknown field 'foo' in value of user defined type " + typename, + "INSERT INTO %s (a, b, c) VALUES (0, ?, 0)", userType("a", 0, "foo", 0)); + } + + // non-frozen UDT with non-frozen nested collection + String typename2 = createType("CREATE TYPE %s (bar int, foo list)"); + String myType2 = KEYSPACE + '.' + typename2; + assertInvalidMessage("Non-frozen UDTs with nested non-frozen collections are not supported", + "CREATE TABLE " + KEYSPACE + ".wrong (k int PRIMARY KEY, v " + myType2 + ")"); } @Test @@ -106,24 +160,61 @@ public class UserTypesTest extends CQLTester { String myType = KEYSPACE + '.' + createType("CREATE TYPE %s (a int)"); createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<" + myType + ">)"); - execute("INSERT INTO %s (a, b) VALUES (1, {a: 1})"); + execute("INSERT INTO %s (a, b) VALUES (1, ?)", userType("a", 1)); assertRows(execute("SELECT b.a FROM %s"), row(1)); flush(); - execute("ALTER TYPE " + myType + " ADD b int"); - execute("INSERT INTO %s (a, b) VALUES (2, {a: 2, b :2})"); + schemaChange("ALTER TYPE " + myType + " ADD b int"); + execute("INSERT INTO %s (a, b) VALUES (2, ?)", userType("a", 2, "b", 2)); - assertRows(execute("SELECT b.a, b.b FROM %s"), - row(1, null), - row(2, 2)); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT b.a, b.b FROM %s"), + row(1, null), + row(2, 2)) + ); + } - flush(); + @Test + public void testAlterNonFrozenUDT() throws Throwable + { + String myType = KEYSPACE + '.' + createType("CREATE TYPE %s (a int, b text)"); + createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + myType + ")"); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", userType("a", 1, "b", "abc")); - assertRows(execute("SELECT b.a, b.b FROM %s"), - row(1, null), - row(2, 2)); + beforeAndAfterFlush(() -> { + assertRows(execute("SELECT v FROM %s"), row(userType("a", 1, "b", "abc"))); + assertRows(execute("SELECT v.a FROM %s"), row(1)); + assertRows(execute("SELECT v.b FROM %s"), row("abc")); + }); + + schemaChange("ALTER TYPE " + myType + " RENAME b TO foo"); + assertRows(execute("SELECT v FROM %s"), row(userType("a", 1, "b", "abc"))); + assertRows(execute("SELECT v.a FROM %s"), row(1)); + assertRows(execute("SELECT v.foo FROM %s"), row("abc")); + + execute("UPDATE %s SET v.foo = 'def' WHERE k = 0"); + assertRows(execute("SELECT v FROM %s"), row(userType("a", 1, "foo", "def"))); + assertRows(execute("SELECT v.a FROM %s"), row(1)); + assertRows(execute("SELECT v.foo FROM %s"), row("def")); + + execute("INSERT INTO %s (k, v) VALUES (0, ?)", userType("a", 2, "foo", "def")); + assertRows(execute("SELECT v FROM %s"), row(userType("a", 2, "foo", "def"))); + assertRows(execute("SELECT v.a FROM %s"), row(2)); + assertRows(execute("SELECT v.foo FROM %s"), row("def")); + + schemaChange("ALTER TYPE " + myType + " ADD c int"); + assertRows(execute("SELECT v FROM %s"), row(userType("a", 2, "foo", "def", "c", null))); + assertRows(execute("SELECT v.a FROM %s"), row(2)); + assertRows(execute("SELECT v.foo FROM %s"), row("def")); + assertRows(execute("SELECT v.c FROM %s"), row(new Object[] {null})); + + execute("INSERT INTO %s (k, v) VALUES (0, ?)", userType("a", 3, "foo", "abc", "c", 0)); + beforeAndAfterFlush(() -> { + assertRows(execute("SELECT v FROM %s"), row(userType("a", 3, "foo", "abc", "c", 0))); + assertRows(execute("SELECT v.c FROM %s"), row(0)); + }); } @Test @@ -134,11 +225,14 @@ public class UserTypesTest extends CQLTester String myOtherType = createType("CREATE TYPE %s (a frozen<" + myType + ">)"); createTable("CREATE TABLE %s (k int PRIMARY KEY, v frozen<" + myType + ">, z frozen<" + myOtherType + ">)"); - assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType, - "INSERT INTO %s (k, v) VALUES (10, {x:?, y:?})", 1, unset()); + if (usePrepared()) + { + assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType, + "INSERT INTO %s (k, v) VALUES (10, {x:?, y:?})", 1, unset()); - assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType, - "INSERT INTO %s (k, v, z) VALUES (10, {x:?, y:?}, {a:{x: ?, y: ?}})", 1, 1, 1, unset()); + assertInvalidMessage("Invalid unset value for field 'y' of user defined type " + myType, + "INSERT INTO %s (k, v, z) VALUES (10, {x:?, y:?}, {a:{x: ?, y: ?}})", 1, 1, 1, unset()); + } } @Test @@ -153,28 +247,22 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (x int PRIMARY KEY, y " + columnType + ")"); - execute("INSERT INTO %s (x, y) VALUES(1, {'firstValue':{a:1}})"); - assertRows(execute("SELECT * FROM %s"), row(1, map("firstValue", userType(1)))); + execute("INSERT INTO %s (x, y) VALUES(1, ?)", map("firstValue", userType("a", 1))); + assertRows(execute("SELECT * FROM %s"), row(1, map("firstValue", userType("a", 1)))); flush(); execute("ALTER TYPE " + KEYSPACE + "." + ut1 + " ADD b int"); - execute("INSERT INTO %s (x, y) VALUES(2, {'secondValue':{a:2, b:2}})"); - execute("INSERT INTO %s (x, y) VALUES(3, {'thirdValue':{a:3}})"); - execute("INSERT INTO %s (x, y) VALUES(4, {'fourthValue':{b:4}})"); + execute("INSERT INTO %s (x, y) VALUES(2, ?)", map("secondValue", userType("a", 2, "b", 2))); + execute("INSERT INTO %s (x, y) VALUES(3, ?)", map("thirdValue", userType("a", 3, "b", null))); + execute("INSERT INTO %s (x, y) VALUES(4, ?)", map("fourthValue", userType("a", null, "b", 4))); - assertRows(execute("SELECT * FROM %s"), - row(1, map("firstValue", userType(1))), - row(2, map("secondValue", userType(2, 2))), - row(3, map("thirdValue", userType(3, null))), - row(4, map("fourthValue", userType(null, 4)))); - - flush(); - - assertRows(execute("SELECT * FROM %s"), - row(1, map("firstValue", userType(1))), - row(2, map("secondValue", userType(2, 2))), - row(3, map("thirdValue", userType(3, null))), - row(4, map("fourthValue", userType(null, 4)))); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT * FROM %s"), + row(1, map("firstValue", userType("a", 1))), + row(2, map("secondValue", userType("a", 2, "b", 2))), + row(3, map("thirdValue", userType("a", 3, "b", null))), + row(4, map("fourthValue", userType("a", null, "b", 4)))) + ); } } @@ -190,28 +278,22 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (x int PRIMARY KEY, y " + columnType + ")"); - execute("INSERT INTO %s (x, y) VALUES(1, {1} )"); - assertRows(execute("SELECT * FROM %s"), row(1, set(userType(1)))); + execute("INSERT INTO %s (x, y) VALUES(1, ?)", set(userType("a", 1))); + assertRows(execute("SELECT * FROM %s"), row(1, set(userType("a", 1)))); flush(); execute("ALTER TYPE " + KEYSPACE + "." + ut1 + " ADD b int"); - execute("INSERT INTO %s (x, y) VALUES(2, {{a:2, b:2}})"); - execute("INSERT INTO %s (x, y) VALUES(3, {{a:3}})"); - execute("INSERT INTO %s (x, y) VALUES(4, {{b:4}})"); + execute("INSERT INTO %s (x, y) VALUES(2, ?)", set(userType("a", 2, "b", 2))); + execute("INSERT INTO %s (x, y) VALUES(3, ?)", set(userType("a", 3, "b", null))); + execute("INSERT INTO %s (x, y) VALUES(4, ?)", set(userType("a", null, "b", 4))); - assertRows(execute("SELECT * FROM %s"), - row(1, set(userType(1))), - row(2, set(userType(2, 2))), - row(3, set(userType(3, null))), - row(4, set(userType(null, 4)))); - - flush(); - - assertRows(execute("SELECT * FROM %s"), - row(1, set(userType(1))), - row(2, set(userType(2, 2))), - row(3, set(userType(3, null))), - row(4, set(userType(null, 4)))); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT * FROM %s"), + row(1, set(userType("a", 1))), + row(2, set(userType("a", 2, "b", 2))), + row(3, set(userType("a", 3, "b", null))), + row(4, set(userType("a", null, "b", 4)))) + ); } } @@ -227,28 +309,22 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (x int PRIMARY KEY, y " + columnType + ")"); - execute("INSERT INTO %s (x, y) VALUES(1, [1] )"); - assertRows(execute("SELECT * FROM %s"), row(1, list(userType(1)))); + execute("INSERT INTO %s (x, y) VALUES(1, ?)", list(userType("a", 1))); + assertRows(execute("SELECT * FROM %s"), row(1, list(userType("a", 1)))); flush(); execute("ALTER TYPE " + KEYSPACE + "." + ut1 + " ADD b int"); - execute("INSERT INTO %s (x, y) VALUES(2, [{a:2, b:2}])"); - execute("INSERT INTO %s (x, y) VALUES(3, [{a:3}])"); - execute("INSERT INTO %s (x, y) VALUES(4, [{b:4}])"); + execute("INSERT INTO %s (x, y) VALUES (2, ?)", list(userType("a", 2, "b", 2))); + execute("INSERT INTO %s (x, y) VALUES (3, ?)", list(userType("a", 3, "b", null))); + execute("INSERT INTO %s (x, y) VALUES (4, ?)", list(userType("a", null, "b", 4))); - assertRows(execute("SELECT * FROM %s"), - row(1, list(userType(1))), - row(2, list(userType(2, 2))), - row(3, list(userType(3, null))), - row(4, list(userType(null, 4)))); - - flush(); - - assertRows(execute("SELECT * FROM %s"), - row(1, list(userType(1))), - row(2, list(userType(2, 2))), - row(3, list(userType(3, null))), - row(4, list(userType(null, 4)))); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT * FROM %s"), + row(1, list(userType("a", 1))), + row(2, list(userType("a", 2, "b", 2))), + row(3, list(userType("a", 3, "b", null))), + row(4, list(userType("a", null, "b", 4)))) + ); } } @@ -259,28 +335,22 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen>)"); - execute("INSERT INTO %s (a, b) VALUES(1, (1, {a:1, b:1}))"); - assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, userType(1, 1)))); + execute("INSERT INTO %s (a, b) VALUES(1, (1, ?))", userType("a", 1, "b", 1)); + assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, userType("a", 1, "b", 1)))); flush(); execute("ALTER TYPE " + KEYSPACE + "." + type + " ADD c int"); - execute("INSERT INTO %s (a, b) VALUES(2, (2, {a: 2, b: 2, c: 2}))"); - execute("INSERT INTO %s (a, b) VALUES(3, (3, {a: 3, b: 3}))"); - execute("INSERT INTO %s (a, b) VALUES(4, (4, {b:4}))"); + execute("INSERT INTO %s (a, b) VALUES (2, (2, ?))", userType("a", 2, "b", 2, "c", 2)); + execute("INSERT INTO %s (a, b) VALUES (3, (3, ?))", userType("a", 3, "b", 3, "c", null)); + execute("INSERT INTO %s (a, b) VALUES (4, (4, ?))", userType("a", null, "b", 4, "c", null)); - assertRows(execute("SELECT * FROM %s"), - row(1, tuple(1, userType(1, 1))), - row(2, tuple(2, userType(2, 2, 2))), - row(3, tuple(3, userType(3, 3, null))), - row(4, tuple(4, userType(null, 4, null)))); - - flush(); - - assertRows(execute("SELECT * FROM %s"), - row(1, tuple(1, userType(1, 1))), - row(2, tuple(2, userType(2, 2, 2))), - row(3, tuple(3, userType(3, 3, null))), - row(4, tuple(4, userType(null, 4, null)))); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT * FROM %s"), + row(1, tuple(1, userType("a", 1, "b", 1))), + row(2, tuple(2, userType("a", 2, "b", 2, "c", 2))), + row(3, tuple(3, userType("a", 3, "b", 3, "c", null))), + row(4, tuple(4, userType("a", null, "b", 4, "c", null)))) + ); } @Test @@ -290,28 +360,22 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen>>)"); - execute("INSERT INTO %s (a, b) VALUES(1, (1, (1, {a:1, b:1})))"); - assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, tuple(1, userType(1, 1))))); + execute("INSERT INTO %s (a, b) VALUES(1, (1, (1, ?)))", userType("a", 1, "b", 1)); + assertRows(execute("SELECT * FROM %s"), row(1, tuple(1, tuple(1, userType("a", 1, "b", 1))))); flush(); execute("ALTER TYPE " + KEYSPACE + "." + type + " ADD c int"); - execute("INSERT INTO %s (a, b) VALUES(2, (2, (1, {a: 2, b: 2, c: 2})))"); - execute("INSERT INTO %s (a, b) VALUES(3, (3, (1, {a: 3, b: 3})))"); - execute("INSERT INTO %s (a, b) VALUES(4, (4, (1, {b:4})))"); + execute("INSERT INTO %s (a, b) VALUES(2, (2, (1, ?)))", userType("a", 2, "b", 2, "c", 2)); + execute("INSERT INTO %s (a, b) VALUES(3, (3, ?))", tuple(1, userType("a", 3, "b", 3, "c", null))); + execute("INSERT INTO %s (a, b) VALUES(4, ?)", tuple(4, tuple(1, userType("a", null, "b", 4, "c", null)))); - assertRows(execute("SELECT * FROM %s"), - row(1, tuple(1, tuple(1, userType(1, 1)))), - row(2, tuple(2, tuple(1, userType(2, 2, 2)))), - row(3, tuple(3, tuple(1, userType(3, 3, null)))), - row(4, tuple(4, tuple(1, userType(null, 4, null))))); - - flush(); - - assertRows(execute("SELECT * FROM %s"), - row(1, tuple(1, tuple(1, userType(1, 1)))), - row(2, tuple(2, tuple(1, userType(2, 2, 2)))), - row(3, tuple(3, tuple(1, userType(3, 3, null)))), - row(4, tuple(4, tuple(1, userType(null, 4, null))))); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT * FROM %s"), + row(1, tuple(1, tuple(1, userType("a", 1, "b", 1)))), + row(2, tuple(2, tuple(1, userType("a", 2, "b", 2, "c", 2)))), + row(3, tuple(3, tuple(1, userType("a", 3, "b", 3, "c", null)))), + row(4, tuple(4, tuple(1, userType("a", null, "b", 4, "c", null))))) + ); } @Test @@ -322,28 +386,24 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (a int PRIMARY KEY, b frozen<" + KEYSPACE + "." + otherType + ">)"); - execute("INSERT INTO %s (a, b) VALUES(1, {x: {a:1, b:1}})"); + execute("INSERT INTO %s (a, b) VALUES(1, {x: ?})", userType("a", 1, "b", 1)); + assertRows(execute("SELECT b.x.a, b.x.b FROM %s"), row(1, 1)); + execute("INSERT INTO %s (a, b) VALUES(1, ?)", userType("x", userType("a", 1, "b", 1))); assertRows(execute("SELECT b.x.a, b.x.b FROM %s"), row(1, 1)); flush(); execute("ALTER TYPE " + KEYSPACE + "." + type + " ADD c int"); - execute("INSERT INTO %s (a, b) VALUES(2, {x: {a: 2, b: 2, c: 2}})"); - execute("INSERT INTO %s (a, b) VALUES(3, {x: {a: 3, b: 3}})"); - execute("INSERT INTO %s (a, b) VALUES(4, {x: {b:4}})"); + execute("INSERT INTO %s (a, b) VALUES(2, {x: ?})", userType("a", 2, "b", 2, "c", 2)); + execute("INSERT INTO %s (a, b) VALUES(3, {x: ?})", userType("a", 3, "b", 3)); + execute("INSERT INTO %s (a, b) VALUES(4, {x: ?})", userType("a", null, "b", 4)); - assertRows(execute("SELECT b.x.a, b.x.b, b.x.c FROM %s"), - row(1, 1, null), - row(2, 2, 2), - row(3, 3, null), - row(null, 4, null)); - - flush(); - - assertRows(execute("SELECT b.x.a, b.x.b, b.x.c FROM %s"), - row(1, 1, null), - row(2, 2, 2), - row(3, 3, null), - row(null, 4, null)); + beforeAndAfterFlush(() -> + assertRows(execute("SELECT b.x.a, b.x.b, b.x.c FROM %s"), + row(1, 1, null), + row(2, 2, 2), + row(3, 3, null), + row(null, 4, null)) + ); } /** @@ -383,10 +443,11 @@ public class UserTypesTest extends CQLTester createTable("CREATE TABLE %s (id int PRIMARY KEY, val frozen<" + type2 + ">)"); - execute("INSERT INTO %s (id, val) VALUES (0, { s : {{ s : {'foo', 'bar'}, m : { 'foo' : 'bar' }, l : ['foo', 'bar']} }})"); + execute("INSERT INTO %s (id, val) VALUES (0, ?)", + userType("s", set(userType("s", set("foo", "bar"), "m", map("foo", "bar"), "l", list("foo", "bar"))))); - // TODO: check result once we have an easy way to do it. For now we just check it doesn't crash - execute("SELECT * FROM %s"); + assertRows(execute("SELECT * FROM %s"), + row(0, userType("s", set(userType("s", set("foo", "bar"), "m", map("foo", "bar"), "l", list("foo", "bar")))))); } /** @@ -398,9 +459,11 @@ public class UserTypesTest extends CQLTester String typeName = createType("CREATE TYPE %s (fooint int, fooset set )"); createTable("CREATE TABLE %s (key int PRIMARY KEY, data frozen <" + typeName + ">)"); - execute("INSERT INTO %s (key, data) VALUES (1, {fooint: 1, fooset: {'2'}})"); + execute("INSERT INTO %s (key, data) VALUES (1, ?)", userType("fooint", 1, "fooset", set("2"))); execute("ALTER TYPE " + keyspace() + "." + typeName + " ADD foomap map "); - execute("INSERT INTO %s (key, data) VALUES (1, {fooint: 1, fooset: {'2'}, foomap: {3 : 'bar'}})"); + execute("INSERT INTO %s (key, data) VALUES (1, ?)", userType("fooint", 1, "fooset", set("2"), "foomap", map(3, "bar"))); + assertRows(execute("SELECT * FROM %s"), + row(1, userType("fooint", 1, "fooset", set("2"), "foomap", map(3, "bar")))); } @Test @@ -464,13 +527,13 @@ public class UserTypesTest extends CQLTester type1 = createType("CREATE TYPE %s (foo ascii)"); String type2 = createType("CREATE TYPE %s (foo frozen<" + type1 + ">)"); - assertComplexInvalidAlterDropStatements(type1, type2, "{foo: 'abc'}"); + assertComplexInvalidAlterDropStatements(type1, type2, "{foo: {foo: 'abc'}}"); type1 = createType("CREATE TYPE %s (foo ascii)"); type2 = createType("CREATE TYPE %s (foo frozen<" + type1 + ">)"); assertComplexInvalidAlterDropStatements(type1, "list>", - "[{foo: 'abc'}]"); + "[{foo: {foo: 'abc'}}]"); type1 = createType("CREATE TYPE %s (foo ascii)"); type2 = createType("CREATE TYPE %s (foo frozen>)"); @@ -515,6 +578,110 @@ public class UserTypesTest extends CQLTester assertInvalidMessage("Cannot drop user type " + typeWithKs(t), "DROP TYPE " + typeWithKs(t) + ';'); } + @Test + public void testInsertNonFrozenUDT() throws Throwable + { + String typeName = createType("CREATE TYPE %s (a int, b text)"); + createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + typeName + ")"); + + execute("INSERT INTO %s (k, v) VALUES (?, {a: ?, b: ?})", 0, 0, "abc"); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc"))); + + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc"))); + + execute("INSERT INTO %s (k, v) VALUES (?, {a: ?, b: ?})", 0, 0, null); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null))); + + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", null, "b", "abc")); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc"))); + } + + @Test + public void testUpdateNonFrozenUDT() throws Throwable + { + String typeName = createType("CREATE TYPE %s (a int, b text)"); + createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + typeName + ")"); + + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc"))); + + // overwrite the whole UDT + execute("UPDATE %s SET v = ? WHERE k = ?", userType("a", 1, "b", "def"), 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 1, "b", "def"))); + + execute("UPDATE %s SET v = ? WHERE k = ?", userType("a", 0, "b", null), 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null))); + + execute("UPDATE %s SET v = ? WHERE k = ?", userType("a", null, "b", "abc"), 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc"))); + + // individually set fields to non-null values + execute("UPDATE %s SET v.a = ? WHERE k = ?", 1, 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 1, "b", "abc"))); + + execute("UPDATE %s SET v.b = ? WHERE k = ?", "def", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 1, "b", "def"))); + + execute("UPDATE %s SET v.a = ?, v.b = ? WHERE k = ?", 0, "abc", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc"))); + + execute("UPDATE %s SET v.b = ?, v.a = ? WHERE k = ?", "abc", 0, 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc"))); + + // individually set fields to null values + execute("UPDATE %s SET v.a = ? WHERE k = ?", null, 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc"))); + + execute("UPDATE %s SET v.a = ? WHERE k = ?", 0, 0); + execute("UPDATE %s SET v.b = ? WHERE k = ?", null, 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null))); + + execute("UPDATE %s SET v.a = ? WHERE k = ?", null, 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null)); + + assertInvalid("UPDATE %s SET v.bad = ? FROM %s WHERE k = ?", 0, 0); + assertInvalid("UPDATE %s SET v = ? FROM %s WHERE k = ?", 0, 0); + assertInvalid("UPDATE %s SET v = ? FROM %s WHERE k = ?", userType("a", 1, "b", "abc", "bad", 123), 0); + } + + @Test + public void testDeleteNonFrozenUDT() throws Throwable + { + String typeName = createType("CREATE TYPE %s (a int, b text)"); + createTable("CREATE TABLE %s (k int PRIMARY KEY, v " + typeName + ")"); + + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", "abc"))); + + execute("DELETE v.b FROM %s WHERE k = ?", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", 0, "b", null))); + + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + execute("DELETE v.a FROM %s WHERE k = ?", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, userType("a", null, "b", "abc"))); + + execute("DELETE v.b FROM %s WHERE k = ?", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null)); + + // delete both fields at once + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + execute("DELETE v.a, v.b FROM %s WHERE k = ?", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null)); + + // same, but reverse field order + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + execute("DELETE v.b, v.a FROM %s WHERE k = ?", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null)); + + // delete the whole thing at once + execute("INSERT INTO %s (k, v) VALUES (?, ?)", 0, userType("a", 0, "b", "abc")); + execute("DELETE v FROM %s WHERE k = ?", 0); + assertRows(execute("SELECT * FROM %s WHERE k = ?", 0), row(0, null)); + + assertInvalid("DELETE v.bad FROM %s WHERE k = ?", 0); + } + private String typeWithKs(String type1) { return keyspace() + '.' + type1; diff --git a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java index ade80bb1d0..9094d4f5ad 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/operations/InsertUpdateIfConditionTest.java @@ -384,6 +384,377 @@ public class InsertUpdateIfConditionTest extends CQLTester assertRows(execute("INSERT INTO %s (partition, key, owner) VALUES ('a', 'c', 'x') IF NOT EXISTS"), row(true)); } + @Test + public void testWholeUDT() throws Throwable + { + String typename = createType("CREATE TYPE %s (a int, b text)"); + String myType = KEYSPACE + '.' + typename; + + for (boolean frozen : new boolean[] {false, true}) + { + createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, v %s)", + frozen + ? "frozen<" + myType + ">" + : myType)); + + Object v = userType("a", 0, "b", "abc"); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v = {a: 0, b: 'abc'}", v); + checkAppliesUDT("v != null", v); + checkAppliesUDT("v != {a: 1, b: 'abc'}", v); + checkAppliesUDT("v != {a: 0, b: 'def'}", v); + checkAppliesUDT("v > {a: -1, b: 'abc'}", v); + checkAppliesUDT("v > {a: 0, b: 'aaa'}", v); + checkAppliesUDT("v > {a: 0}", v); + checkAppliesUDT("v >= {a: 0, b: 'aaa'}", v); + checkAppliesUDT("v >= {a: 0, b: 'abc'}", v); + checkAppliesUDT("v < {a: 0, b: 'zzz'}", v); + checkAppliesUDT("v < {a: 1, b: 'abc'}", v); + checkAppliesUDT("v < {a: 1}", v); + checkAppliesUDT("v <= {a: 0, b: 'zzz'}", v); + checkAppliesUDT("v <= {a: 0, b: 'abc'}", v); + checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {a: 1})", v); + + // multiple conditions + checkAppliesUDT("v > {a: -1, b: 'abc'} AND v > {a: 0}", v); + checkAppliesUDT("v != null AND v IN ({a: 0, b: 'abc'})", v); + + // should not apply + checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v); + checkDoesNotApplyUDT("v = {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v = null", v); + checkDoesNotApplyUDT("v != {a: 0, b: 'abc'}", v); + checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v > {a: 0, b: 'zzz'}", v); + checkDoesNotApplyUDT("v >= {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v >= {a: 0, b: 'zzz'}", v); + checkDoesNotApplyUDT("v < {a: -1, b: 'abc'}", v); + checkDoesNotApplyUDT("v < {a: 0, b: 'aaa'}", v); + checkDoesNotApplyUDT("v <= {a: -1, b: 'abc'}", v); + checkDoesNotApplyUDT("v <= {a: 0, b: 'aaa'}", v); + checkDoesNotApplyUDT("v IN ({a: 0}, {b: 'abc'}, {a: 0, b: 'def'}, null)", v); + checkDoesNotApplyUDT("v IN ()", v); + + // multiple conditions + checkDoesNotApplyUDT("v IN () AND v IN ({a: 0, b: 'abc'})", v); + checkDoesNotApplyUDT("v > {a: 0, b: 'aaa'} AND v < {a: 0, b: 'aaa'}", v); + + // invalid conditions + checkInvalidUDT("v = {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class); + checkInvalidUDT("v = {foo: 'foo'}", v, InvalidRequestException.class); + checkInvalidUDT("v < {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class); + checkInvalidUDT("v < null", v, InvalidRequestException.class); + checkInvalidUDT("v <= {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class); + checkInvalidUDT("v <= null", v, InvalidRequestException.class); + checkInvalidUDT("v > {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class); + checkInvalidUDT("v > null", v, InvalidRequestException.class); + checkInvalidUDT("v >= {a: 1, b: 'abc', c: 'foo'}", v, InvalidRequestException.class); + checkInvalidUDT("v >= null", v, InvalidRequestException.class); + checkInvalidUDT("v IN null", v, SyntaxException.class); + checkInvalidUDT("v IN 367", v, SyntaxException.class); + checkInvalidUDT("v CONTAINS KEY 123", v, SyntaxException.class); + checkInvalidUDT("v CONTAINS 'bar'", v, SyntaxException.class); + + + /////////////////// null suffix on stored udt //////////////////// + v = userType("a", 0, "b", null); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v = {a: 0}", v); + checkAppliesUDT("v = {a: 0, b: null}", v); + checkAppliesUDT("v != null", v); + checkAppliesUDT("v != {a: 1, b: null}", v); + checkAppliesUDT("v != {a: 1}", v); + checkAppliesUDT("v != {a: 0, b: 'def'}", v); + checkAppliesUDT("v > {a: -1, b: 'abc'}", v); + checkAppliesUDT("v > {a: -1}", v); + checkAppliesUDT("v >= {a: 0}", v); + checkAppliesUDT("v >= {a: -1, b: 'abc'}", v); + checkAppliesUDT("v < {a: 0, b: 'zzz'}", v); + checkAppliesUDT("v < {a: 1, b: 'abc'}", v); + checkAppliesUDT("v < {a: 1}", v); + checkAppliesUDT("v <= {a: 0, b: 'zzz'}", v); + checkAppliesUDT("v <= {a: 0}", v); + checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {a: 0})", v); + + // multiple conditions + checkAppliesUDT("v > {a: -1, b: 'abc'} AND v >= {a: 0}", v); + checkAppliesUDT("v != null AND v IN ({a: 0}, {a: 0, b: null})", v); + + // should not apply + checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v); + checkDoesNotApplyUDT("v = {a: 1}", v); + checkDoesNotApplyUDT("v = {b: 'abc'}", v); + checkDoesNotApplyUDT("v = null", v); + checkDoesNotApplyUDT("v != {a: 0}", v); + checkDoesNotApplyUDT("v != {a: 0, b: null}", v); + checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v > {a: 0}", v); + checkDoesNotApplyUDT("v >= {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v >= {a: 1}", v); + checkDoesNotApplyUDT("v < {a: -1, b: 'abc'}", v); + checkDoesNotApplyUDT("v < {a: -1}", v); + checkDoesNotApplyUDT("v < {a: 0}", v); + checkDoesNotApplyUDT("v <= {a: -1, b: 'abc'}", v); + checkDoesNotApplyUDT("v <= {a: -1}", v); + checkDoesNotApplyUDT("v IN ({a: 1}, {b: 'abc'}, {a: 0, b: 'def'}, null)", v); + checkDoesNotApplyUDT("v IN ()", v); + + // multiple conditions + checkDoesNotApplyUDT("v IN () AND v IN ({a: 0})", v); + checkDoesNotApplyUDT("v > {a: -1} AND v < {a: 0}", v); + + + /////////////////// null prefix on stored udt //////////////////// + v = userType("a", null, "b", "abc"); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v = {a: null, b: 'abc'}", v); + checkAppliesUDT("v = {b: 'abc'}", v); + checkAppliesUDT("v != null", v); + checkAppliesUDT("v != {a: 0, b: 'abc'}", v); + checkAppliesUDT("v != {a: 0}", v); + checkAppliesUDT("v != {b: 'def'}", v); + checkAppliesUDT("v > {a: null, b: 'aaa'}", v); + checkAppliesUDT("v > {b: 'aaa'}", v); + checkAppliesUDT("v >= {a: null, b: 'aaa'}", v); + checkAppliesUDT("v >= {b: 'abc'}", v); + checkAppliesUDT("v < {a: null, b: 'zzz'}", v); + checkAppliesUDT("v < {a: 0, b: 'abc'}", v); + checkAppliesUDT("v < {a: 0}", v); + checkAppliesUDT("v < {b: 'zzz'}", v); + checkAppliesUDT("v <= {a: null, b: 'zzz'}", v); + checkAppliesUDT("v <= {a: 0}", v); + checkAppliesUDT("v <= {b: 'abc'}", v); + checkAppliesUDT("v IN (null, {a: null, b: 'abc'}, {a: 0})", v); + checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {b: 'abc'})", v); + + // multiple conditions + checkAppliesUDT("v > {b: 'aaa'} AND v >= {b: 'abc'}", v); + checkAppliesUDT("v != null AND v IN ({a: 0}, {a: null, b: 'abc'})", v); + + // should not apply + checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v); + checkDoesNotApplyUDT("v = {a: 1}", v); + checkDoesNotApplyUDT("v = {b: 'def'}", v); + checkDoesNotApplyUDT("v = null", v); + checkDoesNotApplyUDT("v != {b: 'abc'}", v); + checkDoesNotApplyUDT("v != {a: null, b: 'abc'}", v); + checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v > {a: null, b: 'zzz'}", v); + checkDoesNotApplyUDT("v > {b: 'zzz'}", v); + checkDoesNotApplyUDT("v >= {a: null, b: 'zzz'}", v); + checkDoesNotApplyUDT("v >= {a: 1}", v); + checkDoesNotApplyUDT("v >= {b: 'zzz'}", v); + checkDoesNotApplyUDT("v < {a: null, b: 'aaa'}", v); + checkDoesNotApplyUDT("v < {b: 'aaa'}", v); + checkDoesNotApplyUDT("v <= {a: null, b: 'aaa'}", v); + checkDoesNotApplyUDT("v <= {b: 'aaa'}", v); + checkDoesNotApplyUDT("v IN ({a: 1}, {a: 1, b: 'abc'}, {a: null, b: 'def'}, null)", v); + checkDoesNotApplyUDT("v IN ()", v); + + // multiple conditions + checkDoesNotApplyUDT("v IN () AND v IN ({b: 'abc'})", v); + checkDoesNotApplyUDT("v IN () AND v IN ({a: null, b: 'abc'})", v); + checkDoesNotApplyUDT("v > {a: -1} AND v < {a: 0}", v); + + + /////////////////// null udt //////////////////// + v = null; + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v = null", v); + checkAppliesUDT("v IN (null, {a: null, b: 'abc'}, {a: 0})", v); + checkAppliesUDT("v IN (null, {a: 0, b: 'abc'}, {b: 'abc'})", v); + + // multiple conditions + checkAppliesUDT("v = null AND v IN (null, {a: 0}, {a: null, b: 'abc'})", v); + + // should not apply + checkDoesNotApplyUDT("v = {a: 0, b: 'def'}", v); + checkDoesNotApplyUDT("v = {a: 1}", v); + checkDoesNotApplyUDT("v = {b: 'def'}", v); + checkDoesNotApplyUDT("v != null", v); + checkDoesNotApplyUDT("v > {a: 1, b: 'abc'}", v); + checkDoesNotApplyUDT("v > {a: null, b: 'zzz'}", v); + checkDoesNotApplyUDT("v > {b: 'zzz'}", v); + checkDoesNotApplyUDT("v >= {a: null, b: 'zzz'}", v); + checkDoesNotApplyUDT("v >= {a: 1}", v); + checkDoesNotApplyUDT("v >= {b: 'zzz'}", v); + checkDoesNotApplyUDT("v < {a: null, b: 'aaa'}", v); + checkDoesNotApplyUDT("v < {b: 'aaa'}", v); + checkDoesNotApplyUDT("v <= {a: null, b: 'aaa'}", v); + checkDoesNotApplyUDT("v <= {b: 'aaa'}", v); + checkDoesNotApplyUDT("v IN ({a: 1}, {a: 1, b: 'abc'}, {a: null, b: 'def'})", v); + checkDoesNotApplyUDT("v IN ()", v); + + // multiple conditions + checkDoesNotApplyUDT("v IN () AND v IN ({b: 'abc'})", v); + checkDoesNotApplyUDT("v > {a: -1} AND v < {a: 0}", v); + + } + } + + @Test + public void testUDTField() throws Throwable + { + String typename = createType("CREATE TYPE %s (a int, b text)"); + String myType = KEYSPACE + '.' + typename; + + for (boolean frozen : new boolean[] {false, true}) + { + createTable(String.format("CREATE TABLE %%s (k int PRIMARY KEY, v %s)", + frozen + ? "frozen<" + myType + ">" + : myType)); + + Object v = userType("a", 0, "b", "abc"); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v.a = 0", v); + checkAppliesUDT("v.b = 'abc'", v); + checkAppliesUDT("v.a < 1", v); + checkAppliesUDT("v.b < 'zzz'", v); + checkAppliesUDT("v.b <= 'bar'", v); + checkAppliesUDT("v.b > 'aaa'", v); + checkAppliesUDT("v.b >= 'abc'", v); + checkAppliesUDT("v.a != -1", v); + checkAppliesUDT("v.b != 'xxx'", v); + checkAppliesUDT("v.a != null", v); + checkAppliesUDT("v.b != null", v); + checkAppliesUDT("v.a IN (null, 0, 1)", v); + checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v); + checkAppliesUDT("v.b > 'aaa' AND v.b < 'zzz'", v); + checkAppliesUDT("v.a = 0 AND v.b > 'aaa'", v); + + // do not apply + checkDoesNotApplyUDT("v.a = -1", v); + checkDoesNotApplyUDT("v.b = 'xxx'", v); + checkDoesNotApplyUDT("v.a < -1", v); + checkDoesNotApplyUDT("v.b < 'aaa'", v); + checkDoesNotApplyUDT("v.b <= 'aaa'", v); + checkDoesNotApplyUDT("v.b > 'zzz'", v); + checkDoesNotApplyUDT("v.b >= 'zzz'", v); + checkDoesNotApplyUDT("v.a != 0", v); + checkDoesNotApplyUDT("v.b != 'abc'", v); + checkDoesNotApplyUDT("v.a IN (null, -1)", v); + checkDoesNotApplyUDT("v.b IN (null, 'xxx')", v); + checkDoesNotApplyUDT("v.a IN ()", v); + checkDoesNotApplyUDT("v.b IN ()", v); + checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v); + + // invalid + checkInvalidUDT("v.c = null", v, InvalidRequestException.class); + checkInvalidUDT("v.a < null", v, InvalidRequestException.class); + checkInvalidUDT("v.a <= null", v, InvalidRequestException.class); + checkInvalidUDT("v.a > null", v, InvalidRequestException.class); + checkInvalidUDT("v.a >= null", v, InvalidRequestException.class); + checkInvalidUDT("v.a IN null", v, SyntaxException.class); + checkInvalidUDT("v.a IN 367", v, SyntaxException.class); + checkInvalidUDT("v.b IN (1, 2, 3)", v, InvalidRequestException.class); + checkInvalidUDT("v.a CONTAINS 367", v, SyntaxException.class); + checkInvalidUDT("v.a CONTAINS KEY 367", v, SyntaxException.class); + + + /////////////// null suffix on udt //////////////// + v = userType("a", 0, "b", null); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v.a = 0", v); + checkAppliesUDT("v.b = null", v); + checkAppliesUDT("v.b != 'xxx'", v); + checkAppliesUDT("v.a != null", v); + checkAppliesUDT("v.a IN (null, 0, 1)", v); + checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v); + checkAppliesUDT("v.a = 0 AND v.b = null", v); + + // do not apply + checkDoesNotApplyUDT("v.b = 'abc'", v); + checkDoesNotApplyUDT("v.a < -1", v); + checkDoesNotApplyUDT("v.b < 'aaa'", v); + checkDoesNotApplyUDT("v.b <= 'aaa'", v); + checkDoesNotApplyUDT("v.b > 'zzz'", v); + checkDoesNotApplyUDT("v.b >= 'zzz'", v); + checkDoesNotApplyUDT("v.a != 0", v); + checkDoesNotApplyUDT("v.b != null", v); + checkDoesNotApplyUDT("v.a IN (null, -1)", v); + checkDoesNotApplyUDT("v.b IN ('xxx', 'abc')", v); + checkDoesNotApplyUDT("v.a IN ()", v); + checkDoesNotApplyUDT("v.b IN ()", v); + checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v); + + + /////////////// null prefix on udt //////////////// + v = userType("a", null, "b", "abc"); + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v.a = null", v); + checkAppliesUDT("v.b = 'abc'", v); + checkAppliesUDT("v.a != 0", v); + checkAppliesUDT("v.b != null", v); + checkAppliesUDT("v.a IN (null, 0, 1)", v); + checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v); + checkAppliesUDT("v.a = null AND v.b = 'abc'", v); + + // do not apply + checkDoesNotApplyUDT("v.a = 0", v); + checkDoesNotApplyUDT("v.a < -1", v); + checkDoesNotApplyUDT("v.b >= 'zzz'", v); + checkDoesNotApplyUDT("v.a != null", v); + checkDoesNotApplyUDT("v.b != 'abc'", v); + checkDoesNotApplyUDT("v.a IN (-1, 0)", v); + checkDoesNotApplyUDT("v.b IN (null, 'xxx')", v); + checkDoesNotApplyUDT("v.a IN ()", v); + checkDoesNotApplyUDT("v.b IN ()", v); + checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v); + + + /////////////// null udt //////////////// + v = null; + execute("INSERT INTO %s (k, v) VALUES (0, ?)", v); + + checkAppliesUDT("v.a = null", v); + checkAppliesUDT("v.b = null", v); + checkAppliesUDT("v.a != 0", v); + checkAppliesUDT("v.b != 'abc'", v); + checkAppliesUDT("v.a IN (null, 0, 1)", v); + checkAppliesUDT("v.b IN (null, 'xxx', 'abc')", v); + checkAppliesUDT("v.a = null AND v.b = null", v); + + // do not apply + checkDoesNotApplyUDT("v.a = 0", v); + checkDoesNotApplyUDT("v.a < -1", v); + checkDoesNotApplyUDT("v.b >= 'zzz'", v); + checkDoesNotApplyUDT("v.a != null", v); + checkDoesNotApplyUDT("v.b != null", v); + checkDoesNotApplyUDT("v.a IN (-1, 0)", v); + checkDoesNotApplyUDT("v.b IN ('xxx', 'abc')", v); + checkDoesNotApplyUDT("v.a IN ()", v); + checkDoesNotApplyUDT("v.b IN ()", v); + checkDoesNotApplyUDT("v.b != null AND v.b IN ()", v); + } + } + + void checkAppliesUDT(String condition, Object value) throws Throwable + { + assertRows(execute("UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value), row(true)); + assertRows(execute("SELECT * FROM %s"), row(0, value)); + } + + void checkDoesNotApplyUDT(String condition, Object value) throws Throwable + { + assertRows(execute("UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value), + row(false, value)); + assertRows(execute("SELECT * FROM %s"), row(0, value)); + } + + void checkInvalidUDT(String condition, Object value, Class expected) throws Throwable + { + assertInvalidThrow(expected, "UPDATE %s SET v = ? WHERE k = 0 IF " + condition, value); + assertRows(execute("SELECT * FROM %s"), row(0, value)); + } + /** * Migrated from cql_tests.py:TestCQL.whole_list_conditional_test() */ diff --git a/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java b/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java index 0340fd39c1..9432c90e06 100644 --- a/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java +++ b/test/unit/org/apache/cassandra/schema/LegacySchemaMigratorTest.java @@ -87,7 +87,7 @@ public class LegacySchemaMigratorTest } // make sure that we've read *exactly* the same set of keyspaces/tables/types/functions - assertEquals(expected, actual); + assertEquals(expected.diff(actual).toString(), expected, actual); // check that the build status of all indexes has been updated to use the new // format of index name: the index_name column of system.IndexInfo used to @@ -311,17 +311,20 @@ public class LegacySchemaMigratorTest UserType udt1 = new UserType(keyspace, bytes("udt1"), new ArrayList() {{ add(bytes("col1")); add(bytes("col2")); }}, - new ArrayList>() {{ add(UTF8Type.instance); add(Int32Type.instance); }}); + new ArrayList>() {{ add(UTF8Type.instance); add(Int32Type.instance); }}, + true); UserType udt2 = new UserType(keyspace, bytes("udt2"), new ArrayList() {{ add(bytes("col3")); add(bytes("col4")); }}, - new ArrayList>() {{ add(BytesType.instance); add(BooleanType.instance); }}); + new ArrayList>() {{ add(BytesType.instance); add(BooleanType.instance); }}, + true); UserType udt3 = new UserType(keyspace, bytes("udt3"), new ArrayList() {{ add(bytes("col5")); }}, - new ArrayList>() {{ add(AsciiType.instance); }}); + new ArrayList>() {{ add(AsciiType.instance); }}, + true); return KeyspaceMetadata.create(keyspace, KeyspaceParams.simple(1), @@ -431,12 +434,14 @@ public class LegacySchemaMigratorTest UserType udt1 = new UserType(keyspace, bytes("udt1"), new ArrayList() {{ add(bytes("col1")); add(bytes("col2")); }}, - new ArrayList>() {{ add(UTF8Type.instance); add(Int32Type.instance); }}); + new ArrayList>() {{ add(UTF8Type.instance); add(Int32Type.instance); }}, + true); UserType udt2 = new UserType(keyspace, bytes("udt2"), new ArrayList() {{ add(bytes("col1")); add(bytes("col2")); }}, - new ArrayList>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }}); + new ArrayList>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }}, + true); UDFunction udf1 = UDFunction.create(new FunctionName(keyspace, "udf"), ImmutableList.of(new ColumnIdentifier("col1", false), new ColumnIdentifier("col2", false)), @@ -478,12 +483,14 @@ public class LegacySchemaMigratorTest UserType udt1 = new UserType(keyspace, bytes("udt1"), new ArrayList() {{ add(bytes("col1")); add(bytes("col2")); }}, - new ArrayList>() {{ add(UTF8Type.instance); add(Int32Type.instance); }}); + new ArrayList>() {{ add(UTF8Type.instance); add(Int32Type.instance); }}, + true); UserType udt2 = new UserType(keyspace, bytes("udt2"), new ArrayList() {{ add(bytes("col1")); add(bytes("col2")); }}, - new ArrayList>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }}); + new ArrayList>() {{ add(ListType.getInstance(udt1, false)); add(Int32Type.instance); }}, + true); UDFunction udf1 = UDFunction.create(new FunctionName(keyspace, "udf1"), ImmutableList.of(new ColumnIdentifier("col1", false), new ColumnIdentifier("col2", false)), diff --git a/test/unit/org/apache/cassandra/transport/SerDeserTest.java b/test/unit/org/apache/cassandra/transport/SerDeserTest.java index fdb346e75c..0b19151021 100644 --- a/test/unit/org/apache/cassandra/transport/SerDeserTest.java +++ b/test/unit/org/apache/cassandra/transport/SerDeserTest.java @@ -184,7 +184,8 @@ public class SerDeserTest UserType udt = new UserType("ks", bb("myType"), Arrays.asList(bb("f1"), bb("f2"), bb("f3"), bb("f4")), - Arrays.asList(LongType.instance, lt, st, mt)); + Arrays.asList(LongType.instance, lt, st, mt), + true); Map value = new HashMap<>(); value.put(ci("f1"), lit(42));