Tuple types

patch by slebresne; reviewed by thobbs for CASSANDRA-7248
This commit is contained in:
Sylvain Lebresne 2014-05-23 09:53:07 +02:00
parent 01c5e346fb
commit 0932ed670c
22 changed files with 1414 additions and 1258 deletions

View File

@ -29,6 +29,7 @@
* Handle overlapping MultiSlices (CASSANDRA-7279)
* Fix DataOutputTest on Windows (CASSANDRA-7265)
* Embedded sets in user defined data-types are not updating (CASSANDRA-7267)
* Add tuple type to CQL/native protocol (CASSANDRA-7248)
Merged from 2.0:
* Copy compaction options to make sure they are reloaded (CASSANDRA-7290)
* Add option to do more aggressive tombstone compactions (CASSANDRA-6563)

View File

@ -61,6 +61,7 @@
<property name="test.long.src" value="${test.dir}/long"/>
<property name="test.pig.src" value="${test.dir}/pig"/>
<property name="dist.dir" value="${build.dir}/dist"/>
<property name="source.version" value="1.7"/>
<property name="target.version" value="1.7"/>
@ -92,6 +93,9 @@
<property name="test.timeout" value="60000" />
<property name="test.long.timeout" value="600000" />
<!-- default for cql tests. Can be override by -Dcassandra.test.use_prepared=false -->
<property name="cassandra.test.use_prepared" value="true" />
<!-- http://cobertura.sourceforge.net/ -->
<property name="cobertura.version" value="2.0.3"/>
<property name="cobertura.build.dir" value="${build.dir}/cobertura"/>
@ -444,8 +448,6 @@
artifactId="cassandra-parent"
version="${version}"/>
<dependency groupId="joda-time" artifactId="joda-time" version="2.3" />
<dependency groupId="org.slf4j" artifactId="slf4j-log4j12" version="1.7.2"/>
<dependency groupId="log4j" artifactId="log4j" version="1.2.16" />
</artifact:pom>
<!-- now the pom's for artifacts being deployed to Maven Central -->
@ -1202,6 +1204,41 @@
</testmacro>
</target>
<target name="cql-test" depends="build-test" description="Execute CQL tests">
<testmacro suitename="cql" inputdir="${test.unit.src}"
timeout="${test.timeout}" filter="**/cql3/*Test.java">
<jvmarg value="-Dcassandra.test.use_prepared=${cassandra.test.use_prepared}"/>
</testmacro>
</target>
<target name="cql-test-some" depends="build-test" description="Execute specific CQL tests" >
<sequential>
<echo message="running ${test.methods} tests from ${test.name}"/>
<mkdir dir="${build.test.dir}/cassandra"/>
<mkdir dir="${build.test.dir}/output"/>
<junit fork="on" failureproperty="testfailed" maxmemory="1024m" timeout="${test.timeout}">
<sysproperty key="net.sourceforge.cobertura.datafile" file="${cobertura.datafile}"/>
<formatter type="brief" usefile="false"/>
<jvmarg value="-Dstorage-config=${test.conf}"/>
<jvmarg value="-Djava.awt.headless=true"/>
<jvmarg value="-javaagent:${basedir}/lib/jamm-0.2.6.jar" />
<jvmarg value="-ea"/>
<jvmarg value="-Xss256k"/>
<jvmarg value="-Dcassandra.test.use_prepared=${cassandra.test.use_prepared}"/>
<classpath>
<path refid="cassandra.classpath" />
<pathelement location="${test.classes}"/>
<path refid="cobertura.classpath"/>
<pathelement location="${test.conf}"/>
<fileset dir="${test.lib}">
<include name="**/*.jar" />
</fileset>
</classpath>
<test name="org.apache.cassandra.cql3.${test.name}" methods="${test.methods}" todir="${build.test.dir}/output"/>
</junit>
</sequential>
</target>
<target name="pig-test" depends="build-test,maven-ant-tasks-retrieve-pig-test" description="Excute Pig tests">
<testmacro suitename="pig" inputdir="${test.pig.src}"
timeout="1200000">

View File

@ -38,7 +38,7 @@ Table of Contents
4.2.8. AUTH_SUCCESS
5. Compression
6. Collection types
7. User Defined types
7. User Defined and tuple types
8. Result paging
9. Error codes
10. Changes from v2
@ -306,13 +306,13 @@ Table of Contents
Section 4.2.5.2).
0x04: Page_size. In that case, <result_page_size> is an [int]
controlling the desired page size of the result (in CQL3 rows).
See the section on paging (Section 7) for more details.
See the section on paging (Section 8) for more details.
0x08: With_paging_state. If present, <paging_state> should be present.
<paging_state> is a [bytes] value that should have been returned
in a result set (Section 4.2.5.2). If provided, the query will be
executed but starting from a given paging state. This also to
continue paging on a different node from the one it has been
started (See Section 7 for more details).
started (See Section 8 for more details).
0x10: With serial consistency. If present, <serial_consistency> should be
present. <serial_consistency> is the [consistency] level for the
serial phase of conditional updates. That consitency can only be
@ -529,7 +529,7 @@ Table of Contents
<paging_state> will be present. The <paging_state> is a
[bytes] value that should be used in QUERY/EXECUTE to
continue paging and retrieve the remained of the result for
this query (See Section 7 for more details).
this query (See Section 8 for more details).
0x0004 No_metadata: if set, the <metadata> is only composed of
these <flags>, the <column_count> and optionally the
<paging_state> (depending on the Has_more_pages flage) but
@ -592,6 +592,10 @@ Table of Contents
i_th field of the UDT.
- <type_i> is an [option] representing the type of the
i_th field of the UDT.
0x0031 Tuple: the value is <n><type_1>...<type_n> where <n> is a [short]
representing the number of value in the type, and <type_i>
are [option] representing the type of the i_th component
of the tuple
- <rows_count> is an [int] representing the number of rows present in this
result. Those rows are serialized in the <rows_content> part.
@ -756,16 +760,20 @@ Table of Contents
value.
7. User defined types
7. User defined and tuple types
This section describes the serialization format for User defined types (UDT) values.
UDT values are the values of the User Defined Types as defined in section 4.2.5.2.
This section describes the serialization format for User defined types (UDT) and
tuple values. UDT (resp. tuple) values are the values of the User Defined Types
(resp. tuple type) as defined in section 4.2.5.2.
A UDT value is composed of successive [bytes] values, one for each field of the UDT
value (in the order defined by the type). A UDT value will generally have one value
for each field of the type it represents, but it is allowed to have less values than
the type has fields.
A tuple value has the exact same serialization format, i.e. a succession of
[bytes] values representing the components of the tuple.
8. Result paging
@ -896,8 +904,8 @@ Table of Contents
10. Changes from v2
* BATCH messages now have <flags> (like QUERY and EXECUTE) and a corresponding optional
<serial_consistency> parameters (see Section 4.1.7).
* User Defined Types have to added to ResultSet metadata (see 4.2.5.2) and a new section
on the serialization format of UDT values has been added to the documentation
* User Defined Types and tuple types have to added to ResultSet metadata (see 4.2.5.2) and a
new section on the serialization format of UDT and tuple values has been added to the documentation
(Section 7).
* The serialization format for collection has changed (both the collection size and
the length of each argument is now 4 bytes long). See Section 6.

View File

@ -100,11 +100,11 @@ public final class UTMetaData
adder.resetCollection("field_names");
adder.resetCollection("field_types");
for (ByteBuffer name : newType.fieldNames)
adder.addListEntry("field_names", name);
for (AbstractType<?> type : newType.fieldTypes)
adder.addListEntry("field_types", type.toString());
for (int i = 0; i < newType.size(); i++)
{
adder.addListEntry("field_names", newType.fieldName(i));
adder.addListEntry("field_types", newType.fieldType(i).toString());
}
return mutation;
}

View File

@ -17,6 +17,9 @@
*/
package org.apache.cassandra.cql3;
import java.util.ArrayList;
import java.util.List;
import org.apache.cassandra.config.KSMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.db.marshal.*;
@ -221,6 +224,62 @@ public interface CQL3Type
}
}
public static class Tuple implements CQL3Type
{
private final TupleType type;
private Tuple(TupleType type)
{
this.type = type;
}
public static Tuple create(TupleType type)
{
return new Tuple(type);
}
public boolean isCollection()
{
return false;
}
public AbstractType<?> getType()
{
return type;
}
@Override
public final boolean equals(Object o)
{
if(!(o instanceof Tuple))
return false;
Tuple that = (Tuple)o;
return type.equals(that.type);
}
@Override
public final int hashCode()
{
return type.hashCode();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("tuple<");
for (int i = 0; i < type.size(); i++)
{
if (i > 0)
sb.append(", ");
sb.append(type.type(i).asCQL3Type());
}
sb.append(">");
return sb.toString();
}
}
// For UserTypes, we need to know the current keyspace to resolve the
// actual type used, so Raw is a "not yet prepared" CQL3Type.
public abstract class Raw
@ -277,6 +336,15 @@ public interface CQL3Type
return new RawCollection(CollectionType.Kind.SET, null, t);
}
public static Raw tuple(List<CQL3Type.Raw> ts) throws InvalidRequestException
{
for (int i = 0; i < ts.size(); i++)
if (ts.get(i).isCounter())
throw new InvalidRequestException("counters are not allowed inside tuples");
return new RawTuple(ts);
}
private static class RawType extends Raw
{
private CQL3Type type;
@ -386,5 +454,43 @@ public interface CQL3Type
return name.toString();
}
}
private static class RawTuple extends Raw
{
private final List<CQL3Type.Raw> types;
private RawTuple(List<CQL3Type.Raw> types)
{
this.types = types;
}
public boolean isCollection()
{
return false;
}
public CQL3Type prepare(String keyspace) throws InvalidRequestException
{
List<AbstractType<?>> ts = new ArrayList<>(types.size());
for (CQL3Type.Raw t : types)
ts.add(t.prepare(keyspace).getType());
return new Tuple(new TupleType(ts));
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("tuple<");
for (int i = 0; i < types.size(); i++)
{
if (i > 0)
sb.append(", ");
sb.append(types.get(i));
}
sb.append(">");
return sb.toString();
}
}
}
}

View File

@ -846,13 +846,13 @@ constant returns [Constants.Literal constant]
| { String sign=""; } ('-' {sign = "-"; } )? t=(K_NAN | K_INFINITY) { $constant = Constants.Literal.floatingPoint(sign + $t.text); }
;
map_literal returns [Maps.Literal map]
mapLiteral returns [Maps.Literal map]
: '{' { List<Pair<Term.Raw, Term.Raw>> m = new ArrayList<Pair<Term.Raw, Term.Raw>>(); }
( k1=term ':' v1=term { m.add(Pair.create(k1, v1)); } ( ',' kn=term ':' vn=term { m.add(Pair.create(kn, vn)); } )* )?
'}' { $map = new Maps.Literal(m); }
;
set_or_map[Term.Raw t] returns [Term.Raw value]
setOrMapLiteral[Term.Raw t] returns [Term.Raw value]
: ':' v=term { List<Pair<Term.Raw, Term.Raw>> m = new ArrayList<Pair<Term.Raw, Term.Raw>>(); m.add(Pair.create(t, v)); }
( ',' kn=term ':' vn=term { m.add(Pair.create(kn, vn)); } )*
{ $value = new Maps.Literal(m); }
@ -861,27 +861,34 @@ set_or_map[Term.Raw t] returns [Term.Raw value]
{ $value = new Sets.Literal(s); }
;
collection_literal returns [Term.Raw value]
collectionLiteral returns [Term.Raw value]
: '[' { List<Term.Raw> l = new ArrayList<Term.Raw>(); }
( t1=term { l.add(t1); } ( ',' tn=term { l.add(tn); } )* )?
']' { $value = new Lists.Literal(l); }
| '{' t=term v=set_or_map[t] { $value = v; } '}'
| '{' t=term v=setOrMapLiteral[t] { $value = v; } '}'
// Note that we have an ambiguity between maps and set for "{}". So we force it to a set literal,
// and deal with it later based on the type of the column (SetLiteral.java).
| '{' '}' { $value = new Sets.Literal(Collections.<Term.Raw>emptyList()); }
;
usertype_literal returns [UserTypes.Literal ut]
usertypeLiteral returns [UserTypes.Literal ut]
@init{ Map<ColumnIdentifier, Term.Raw> m = new HashMap<ColumnIdentifier, Term.Raw>(); }
@after{ $ut = new UserTypes.Literal(m); }
// We don't allow empty literals because that conflicts with sets/maps and is currently useless since we don't allow empty user types
: '{' k1=cident ':' v1=term { m.put(k1, v1); } ( ',' kn=cident ':' vn=term { m.put(kn, vn); } )* '}'
;
tupleLiteral returns [Tuples.Literal tt]
@init{ List<Term.Raw> l = new ArrayList<Term.Raw>(); }
@after{ $tt = new Tuples.Literal(l); }
: '(' t1=term { l.add(t1); } ( ',' tn=term { l.add(tn); } )* ')'
;
value returns [Term.Raw value]
: c=constant { $value = c; }
| l=collection_literal { $value = l; }
| u=usertype_literal { $value = u; }
| l=collectionLiteral { $value = l; }
| u=usertypeLiteral { $value = u; }
| t=tupleLiteral { $value = t; }
| K_NULL { $value = Constants.NULL_LITERAL; }
| ':' id=cident { $value = newBindVariables(id); }
| QMARK { $value = newBindVariables(null); }
@ -959,7 +966,7 @@ properties[PropertyDefinitions props]
property[PropertyDefinitions props]
: k=cident '=' (simple=propertyValue { try { $props.addProperty(k.toString(), simple); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } }
| map=map_literal { try { $props.addProperty(k.toString(), convertPropertyMap(map)); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } })
| map=mapLiteral { try { $props.addProperty(k.toString(), convertPropertyMap(map)); } catch (SyntaxException e) { addRecognitionError(e.getMessage()); } })
;
propertyValue returns [String str]
@ -1026,11 +1033,6 @@ singleColumnInValues returns [List<Term.Raw> terms]
: '(' ( t1 = term { $terms.add(t1); } (',' ti=term { $terms.add(ti); })* )? ')'
;
tupleLiteral returns [Tuples.Literal literal]
@init { List<Term.Raw> terms = new ArrayList<>(); }
: '(' t1=term { terms.add(t1); } (',' ti=term { terms.add(ti); })* ')' { $literal = new Tuples.Literal(terms); }
;
tupleOfTupleLiterals returns [List<Tuples.Literal> literals]
@init { $literals = new ArrayList<>(); }
: '(' t1=tupleLiteral { $literals.add(t1); } (',' ti=tupleLiteral { $literals.add(ti); })* ')'
@ -1054,7 +1056,8 @@ inMarkerForTuple returns [Tuples.INRaw marker]
comparatorType returns [CQL3Type.Raw t]
: n=native_type { $t = CQL3Type.Raw.from(n); }
| c=collection_type { $t = c; }
| id=userTypeName { $t = CQL3Type.Raw.userType(id); }
| tt=tuple_type { $t = tt; }
| id=userTypeName { $t = CQL3Type.Raw.userType(id); }
| s=STRING_LITERAL
{
try {
@ -1099,6 +1102,12 @@ collection_type returns [CQL3Type.Raw pt]
{ try { if (t != null) $pt = CQL3Type.Raw.set(t); } catch (InvalidRequestException e) { addRecognitionError(e.getMessage()); } }
;
tuple_type returns [CQL3Type.Raw t]
: K_TUPLE '<' { List<CQL3Type.Raw> types = new ArrayList<>(); }
t1=comparatorType { types.add(t1); } (',' tn=comparatorType { types.add(tn); })*
'>' { try { $t = CQL3Type.Raw.tuple(types); } catch (InvalidRequestException e) { addRecognitionError(e.getMessage()); }}
;
username
: IDENT
| STRING_LITERAL
@ -1110,11 +1119,12 @@ non_type_ident returns [ColumnIdentifier id]
: t=IDENT { if (reservedTypeNames.contains($t.text)) addRecognitionError("Invalid (reserved) user type name " + $t.text); $id = new ColumnIdentifier($t.text, false); }
| t=QUOTED_NAME { $id = new ColumnIdentifier($t.text, true); }
| k=basic_unreserved_keyword { $id = new ColumnIdentifier(k, false); }
| kk=K_KEY { $id = new ColumnIdentifier($kk.text, false); }
;
unreserved_keyword returns [String str]
: u=unreserved_function_keyword { $str = u; }
| k=(K_TTL | K_COUNT | K_WRITETIME) { $str = $k.text; }
| k=(K_TTL | K_COUNT | K_WRITETIME | K_KEY) { $str = $k.text; }
;
unreserved_function_keyword returns [String str]
@ -1123,8 +1133,7 @@ unreserved_function_keyword returns [String str]
;
basic_unreserved_keyword returns [String str]
: k=( K_KEY
| K_KEYS
: k=( K_KEYS
| K_AS
| K_CLUSTERING
| K_COMPACT
@ -1251,6 +1260,7 @@ K_MAP: M A P;
K_LIST: L I S T;
K_NAN: N A N;
K_INFINITY: I N F I N I T Y;
K_TUPLE: T U P L E;
K_TRIGGER: T R I G G E R;
K_STATIC: S T A T I C;

View File

@ -17,17 +17,15 @@
*/
package org.apache.cassandra.cql3;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.TupleType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.serializers.MarshalException;
import java.nio.ByteBuffer;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.*;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.serializers.MarshalException;
/**
* Static helper methods and classes for tuples.
@ -36,6 +34,16 @@ public class Tuples
{
private static final Logger logger = LoggerFactory.getLogger(Tuples.class);
private Tuples() {}
public static ColumnSpecification componentSpecOf(ColumnSpecification column, int component)
{
return new ColumnSpecification(column.ksName,
column.cfName,
new ColumnIdentifier(String.format("%s[%d]", column.name, component), true),
((TupleType)column.type).type(component));
}
/**
* A raw, literal tuple. When prepared, this will become a Tuples.Value or Tuples.DelayedValue, depending
* on whether the tuple holds NonTerminals.
@ -49,6 +57,24 @@ public class Tuples
this.elements = elements;
}
public Term prepare(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
{
validateAssignableTo(keyspace, receiver);
List<Term> values = new ArrayList<>(elements.size());
boolean allTerminal = true;
for (int i = 0; i < elements.size(); i++)
{
Term value = elements.get(i).prepare(keyspace, componentSpecOf(receiver, i));
if (value instanceof Term.NonTerminal)
allTerminal = false;
values.add(value);
}
DelayedValue value = new DelayedValue(values);
return allTerminal ? value.bind(QueryOptions.DEFAULT) : value;
}
public Term prepare(String keyspace, List<? extends ColumnSpecification> receivers) throws InvalidRequestException
{
if (elements.size() != receivers.size())
@ -68,15 +94,36 @@ public class Tuples
return allTerminal ? value.bind(QueryOptions.DEFAULT) : value;
}
public Term prepare(String keyspace, ColumnSpecification receiver)
private void validateAssignableTo(String keyspace, ColumnSpecification receiver) throws InvalidRequestException
{
throw new AssertionError("Tuples.Literal instances require a list of receivers for prepare()");
if (!(receiver.type instanceof TupleType))
throw new InvalidRequestException(String.format("Invalid tuple type literal for %s of type %s", receiver.name, receiver.type.asCQL3Type()));
TupleType tt = (TupleType)receiver.type;
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()));
Term.Raw value = elements.get(i);
ColumnSpecification spec = componentSpecOf(receiver, i);
if (!value.isAssignableTo(keyspace, spec))
throw new InvalidRequestException(String.format("Invalid tuple literal for %s: component %d is not of type %s", receiver.name, i, spec.type.asCQL3Type()));
}
}
public boolean isAssignableTo(String keyspace, ColumnSpecification receiver)
{
// tuples shouldn't be assignable to anything right now
return false;
try
{
validateAssignableTo(keyspace, receiver);
return true;
}
catch (InvalidRequestException e)
{
return false;
}
}
@Override
@ -105,7 +152,7 @@ public class Tuples
public ByteBuffer get(QueryOptions options)
{
throw new UnsupportedOperationException();
return TupleType.buildValue(elements);
}
public List<ByteBuffer> getElements()
@ -141,18 +188,28 @@ public class Tuples
term.collectMarkerSpecification(boundNames);
}
private ByteBuffer[] bindInternal(QueryOptions options) throws InvalidRequestException
{
// Inside tuples, we must force the serialization of collections whatever the protocol version is in
// use since we're going to store directly that serialized value.
options = options.withProtocolVersion(3);
ByteBuffer[] buffers = new ByteBuffer[elements.size()];
for (int i = 0; i < elements.size(); i++)
buffers[i] = elements.get(i).bindAndGet(options);
return buffers;
}
public Value bind(QueryOptions options) throws InvalidRequestException
{
ByteBuffer[] buffers = new ByteBuffer[elements.size()];
for (int i=0; i < elements.size(); i++)
{
ByteBuffer bytes = elements.get(i).bindAndGet(options);
if (bytes == null)
throw new InvalidRequestException("Tuples may not contain null values");
return new Value(bindInternal(options));
}
buffers[i] = elements.get(i).bindAndGet(options);
}
return new Value(buffers);
@Override
public ByteBuffer bindAndGet(QueryOptions options) throws InvalidRequestException
{
// We don't "need" that override but it saves us the allocation of a Value object if used
return TupleType.buildValue(bindInternal(options));
}
@Override
@ -343,4 +400,4 @@ public class Tuples
sb.append(')');
return sb.toString();
}
}
}

View File

@ -55,6 +55,9 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
public abstract int size();
public abstract Row one();
// No implemented by all subclasses, but we use it when we know it's there (for tests)
public abstract List<ColumnSpecification> metadata();
private static class FromResultSet extends UntypedResultSet
{
private final ResultSet cqlRows;
@ -90,6 +93,11 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
}
};
}
public List<ColumnSpecification> metadata()
{
return cqlRows.metadata.names;
}
}
private static class FromResultList extends UntypedResultSet
@ -127,6 +135,11 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
}
};
}
public List<ColumnSpecification> metadata()
{
throw new UnsupportedOperationException();
}
}
private static class FromPager extends UntypedResultSet
@ -176,6 +189,11 @@ public abstract class UntypedResultSet implements Iterable<UntypedResultSet.Row>
}
};
}
public List<ColumnSpecification> metadata()
{
return metadata;
}
}
public static class Row

View File

@ -36,7 +36,7 @@ public abstract class UserTypes
return new ColumnSpecification(column.ksName,
column.cfName,
new ColumnIdentifier(column.name + "." + field, true),
((UserType)column.type).fieldTypes.get(field));
((UserType)column.type).fieldType(field));
}
public static class Literal implements Term.Raw
@ -55,9 +55,9 @@ public abstract class UserTypes
UserType ut = (UserType)receiver.type;
boolean allTerminal = true;
List<Term> values = new ArrayList<>(entries.size());
for (int i = 0; i < ut.fieldTypes.size(); i++)
for (int i = 0; i < ut.size(); i++)
{
ColumnIdentifier field = new ColumnIdentifier(ut.fieldNames.get(i), UTF8Type.instance);
ColumnIdentifier field = new ColumnIdentifier(ut.fieldName(i), UTF8Type.instance);
Term.Raw raw = entries.get(field);
if (raw == null)
raw = Constants.NULL_LITERAL;
@ -78,9 +78,9 @@ public abstract class UserTypes
throw new InvalidRequestException(String.format("Invalid user type literal for %s of type %s", receiver, receiver.type.asCQL3Type()));
UserType ut = (UserType)receiver.type;
for (int i = 0; i < ut.fieldTypes.size(); i++)
for (int i = 0; i < ut.size(); i++)
{
ColumnIdentifier field = new ColumnIdentifier(ut.fieldNames.get(i), UTF8Type.instance);
ColumnIdentifier field = new ColumnIdentifier(ut.fieldName(i), UTF8Type.instance);
Term.Raw value = entries.get(field);
if (value == null)
continue;
@ -144,7 +144,7 @@ public abstract class UserTypes
public void collectMarkerSpecification(VariableSpecifications boundNames)
{
for (int i = 0; i < type.fieldTypes.size(); i++)
for (int i = 0; i < type.size(); i++)
values.get(i).collectMarkerSpecification(boundNames);
}
@ -155,7 +155,7 @@ public abstract class UserTypes
options = options.withProtocolVersion(3);
ByteBuffer[] buffers = new ByteBuffer[values.size()];
for (int i = 0; i < type.fieldTypes.size(); i++)
for (int i = 0; i < type.size(); i++)
buffers[i] = values.get(i).bindAndGet(options);
return buffers;
}

View File

@ -138,8 +138,8 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement
private static int getIdxOfField(UserType type, ColumnIdentifier field)
{
for (int i = 0; i < type.fieldTypes.size(); i++)
if (field.bytes.equals(type.fieldNames.get(i)))
for (int i = 0; i < type.size(); i++)
if (field.bytes.equals(type.fieldName(i)))
return i;
return -1;
}
@ -183,8 +183,8 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement
return updated;
// Otherwise, check for nesting
List<AbstractType<?>> updatedTypes = updateTypes(ut.fieldTypes, keyspace, toReplace, updated);
return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames), updatedTypes);
List<AbstractType<?>> updatedTypes = updateTypes(ut.fieldTypes(), keyspace, toReplace, updated);
return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames()), updatedTypes);
}
else if (type instanceof CompositeType)
{
@ -275,12 +275,12 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement
if (getIdxOfField(toUpdate, fieldName) >= 0)
throw new InvalidRequestException(String.format("Cannot add new field %s to type %s: a field of the same name already exists", fieldName, name));
List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames.size() + 1);
newNames.addAll(toUpdate.fieldNames);
List<ByteBuffer> newNames = new ArrayList<>(toUpdate.size() + 1);
newNames.addAll(toUpdate.fieldNames());
newNames.add(fieldName.bytes);
List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes.size() + 1);
newTypes.addAll(toUpdate.fieldTypes);
List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.size() + 1);
newTypes.addAll(toUpdate.fieldTypes());
newTypes.add(type.prepare(keyspace()).getType());
return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes);
@ -292,12 +292,12 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement
if (idx < 0)
throw new InvalidRequestException(String.format("Unknown field %s in type %s", fieldName, name));
AbstractType<?> previous = toUpdate.fieldTypes.get(idx);
AbstractType<?> previous = toUpdate.fieldType(idx);
if (!type.prepare(keyspace()).getType().isCompatibleWith(previous))
throw new InvalidRequestException(String.format("Type %s is incompatible with previous type %s of field %s in user type %s", type, previous.asCQL3Type(), fieldName, name));
List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames);
List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes);
List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames());
List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes());
newTypes.set(idx, type.prepare(keyspace()).getType());
return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes);
@ -321,8 +321,8 @@ public abstract class AlterTypeStatement extends SchemaAlteringStatement
protected UserType makeUpdatedType(UserType toUpdate) throws InvalidRequestException
{
List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames);
List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes);
List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames());
List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes());
for (Map.Entry<ColumnIdentifier, ColumnIdentifier> entry : renames.entrySet())
{

View File

@ -74,12 +74,12 @@ public class CreateTypeStatement extends SchemaAlteringStatement
public static void checkForDuplicateNames(UserType type) throws InvalidRequestException
{
for (int i = 0; i < type.fieldTypes.size() - 1; i++)
for (int i = 0; i < type.size() - 1; i++)
{
ByteBuffer fieldName = type.fieldNames.get(i);
for (int j = i+1; j < type.fieldTypes.size(); j++)
ByteBuffer fieldName = type.fieldName(i);
for (int j = i+1; j < type.size(); j++)
{
if (fieldName.equals(type.fieldNames.get(j)))
if (fieldName.equals(type.fieldName(j)))
throw new InvalidRequestException(String.format("Duplicate field name %s in type %s",
UTF8Type.instance.getString(fieldName),
UTF8Type.instance.getString(type.name)));

View File

@ -97,7 +97,7 @@ public class DropTypeStatement extends SchemaAlteringStatement
if (name.getKeyspace().equals(ut.keyspace) && name.getUserTypeName().equals(ut.name))
return true;
for (AbstractType<?> subtype : ut.fieldTypes)
for (AbstractType<?> subtype : ut.fieldTypes())
if (isUsedBy(subtype))
return true;
}

View File

@ -778,7 +778,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
}
private static List<Composite> buildBound(Bound bound,
Collection<ColumnDefinition> defs,
List<ColumnDefinition> defs,
Restriction[] restrictions,
boolean isReversed,
CType type,
@ -797,7 +797,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
else if (firstRestriction.isIN())
return buildMultiColumnInBound(bound, defs, (MultiColumnRestriction.IN) firstRestriction, isReversed, builder, type, options);
else
return buildMultiColumnEQBound(bound, (MultiColumnRestriction.EQ) firstRestriction, isReversed, builder, options);
return buildMultiColumnEQBound(bound, defs, (MultiColumnRestriction.EQ) firstRestriction, isReversed, builder, options);
}
}
@ -886,7 +886,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
}
private static List<Composite> buildMultiColumnSliceBound(Bound bound,
Collection<ColumnDefinition> defs,
List<ColumnDefinition> defs,
MultiColumnRestriction.Slice slice,
boolean isReversed,
CBuilder builder,
@ -911,22 +911,29 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
}
List<ByteBuffer> vals = slice.componentBounds(firstComponentBound, options);
builder.add(vals.get(firstName.position()));
while(iter.hasNext())
ByteBuffer v = vals.get(firstName.position());
if (v == null)
throw new InvalidRequestException("Invalid null value in condition for column " + firstName.name);
builder.add(v);
while (iter.hasNext())
{
ColumnDefinition def = iter.next();
if (def.position() >= vals.size())
break;
builder.add(vals.get(def.position()));
v = vals.get(def.position());
if (v == null)
throw new InvalidRequestException("Invalid null value in condition for column " + def.name);
builder.add(v);
}
Relation.Type relType = slice.getRelation(eocBound, firstComponentBound);
return Collections.singletonList(builder.build().withEOC(eocForRelation(relType)));
}
private static List<Composite> buildMultiColumnInBound(Bound bound,
Collection<ColumnDefinition> defs,
List<ColumnDefinition> defs,
MultiColumnRestriction.IN restriction,
boolean isReversed,
CBuilder builder,
@ -941,6 +948,10 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
Iterator<ColumnDefinition> iter = defs.iterator();
for (List<ByteBuffer> components : splitInValues)
{
for (int i = 0; i < components.size(); i++)
if (components.get(i) == null)
throw new InvalidRequestException("Invalid null value in condition for column " + defs.get(i));
Composite prefix = builder.buildWith(components);
Bound b = isReversed == isReversedType(iter.next()) ? bound : Bound.reverse(bound);
inValues.add(b == Bound.END && builder.remainingCount() - components.size() > 0
@ -951,14 +962,21 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
}
private static List<Composite> buildMultiColumnEQBound(Bound bound,
List<ColumnDefinition> defs,
MultiColumnRestriction.EQ restriction,
boolean isReversed,
CBuilder builder,
QueryOptions options) throws InvalidRequestException
{
Bound eocBound = isReversed ? Bound.reverse(bound) : bound;
for (ByteBuffer component : restriction.values(options))
List<ByteBuffer> values = restriction.values(options);
for (int i = 0; i < values.size(); i++)
{
ByteBuffer component = values.get(i);
if (component == null)
throw new InvalidRequestException("Invalid null value in condition for column " + defs.get(i));
builder.add(component);
}
Composite prefix = builder.build();
return Collections.singletonList(builder.remainingCount() > 0 && eocBound == Bound.END

View File

@ -142,13 +142,13 @@ public abstract class Selection
throw new InvalidRequestException(String.format("Invalid field selection: %s of type %s is not a user type", withField.selected, type.asCQL3Type()));
UserType ut = (UserType)type;
for (int i = 0; i < ut.fieldTypes.size(); i++)
for (int i = 0; i < ut.size(); i++)
{
if (!ut.fieldNames.get(i).equals(withField.field.bytes))
if (!ut.fieldName(i).equals(withField.field.bytes))
continue;
if (metadata != null)
metadata.add(makeFieldSelectSpec(cfm, withField, ut.fieldTypes.get(i), raw.alias));
metadata.add(makeFieldSelectSpec(cfm, withField, ut.fieldType(i), raw.alias));
return new FieldSelector(ut, i, selected);
}
throw new InvalidRequestException(String.format("%s of type %s has no field %s", withField.selected, type.asCQL3Type(), withField.field));
@ -472,13 +472,13 @@ public abstract class Selection
public AbstractType<?> getType()
{
return type.fieldTypes.get(field);
return type.fieldType(field);
}
@Override
public String toString()
{
return String.format("%s.%s", selected, UTF8Type.instance.getString(type.fieldNames.get(field)));
return String.format("%s.%s", selected, UTF8Type.instance.getString(type.fieldName(field)));
}
}

View File

@ -120,9 +120,10 @@ public class TupleType extends AbstractType<ByteBuffer>
throw new MarshalException(String.format("Not enough bytes to read size of %dth component", i));
int size = input.getInt();
// We don't handle null just yet, but we should fix that soon (CASSANDRA-7206)
// size < 0 means null value
if (size < 0)
throw new MarshalException("Nulls are not yet supported inside tuple values");
continue;
if (input.remaining() < size)
throw new MarshalException(String.format("Not enough bytes to read %dth component", i));
@ -158,13 +159,20 @@ public class TupleType extends AbstractType<ByteBuffer>
{
int totalLength = 0;
for (ByteBuffer component : components)
totalLength += 4 + component.remaining();
totalLength += 4 + (component == null ? 0 : component.remaining());
ByteBuffer result = ByteBuffer.allocate(totalLength);
for (ByteBuffer component : components)
{
result.putInt(component.remaining());
result.put(component.duplicate());
if (component == null)
{
result.putInt(-1);
}
else
{
result.putInt(component.remaining());
result.put(component.duplicate());
}
}
result.rewind();
return result;
@ -183,12 +191,17 @@ public class TupleType extends AbstractType<ByteBuffer>
if (i > 0)
sb.append(":");
AbstractType<?> type = type(i);
int size = input.getInt();
assert size >= 0; // We don't support nulls yet, but we will likely do with #7206 and we'll need
// a way to represent it as a string (without it conflicting with a user value)
if (size < 0)
{
sb.append("@");
continue;
}
ByteBuffer field = ByteBufferUtil.readBytes(input, size);
// We use ':' as delimiter so escape it if it's in the generated string
sb.append(field == null ? "null" : type(i).getString(value).replaceAll(":", "\\\\:"));
// We use ':' as delimiter, and @ to represent null, so escape them in the generated string
sb.append(type.getString(field).replaceAll(":", "\\\\:").replaceAll("@", "\\\\@"));
}
return sb.toString();
}
@ -196,15 +209,19 @@ public class TupleType extends AbstractType<ByteBuffer>
public ByteBuffer fromString(String source)
{
// Split the input on non-escaped ':' characters
List<String> strings = AbstractCompositeType.split(source);
ByteBuffer[] components = new ByteBuffer[strings.size()];
for (int i = 0; i < strings.size(); i++)
List<String> fieldStrings = AbstractCompositeType.split(source);
ByteBuffer[] fields = new ByteBuffer[fieldStrings.size()];
for (int i = 0; i < fieldStrings.size(); i++)
{
// TODO: we'll need to handle null somehow here once we support them
String str = strings.get(i).replaceAll("\\\\:", ":");
components[i] = type(i).fromString(str);
String fieldString = fieldStrings.get(i);
// We use @ to represent nulls
if (fieldString.equals("@"))
continue;
AbstractType<?> type = type(i);
fields[i] = type.fromString(fieldString.replaceAll("\\\\:", ":").replaceAll("\\\\@", "@"));
}
return buildValue(components);
return buildValue(fields);
}
public TypeSerializer<ByteBuffer> getSerializer()
@ -270,10 +287,15 @@ public class TupleType extends AbstractType<ByteBuffer>
return types.equals(that.types);
}
@Override
public CQL3Type asCQL3Type()
{
return CQL3Type.Tuple.create(this);
}
@Override
public String toString()
{
return getClass().getName() + TypeParser.stringifyTypeParameters(types);
}
}

View File

@ -33,21 +33,22 @@ import org.apache.cassandra.utils.Pair;
/**
* A user defined type.
*
* A user type is really just a tuple type on steroids.
*/
public class UserType extends AbstractType<ByteBuffer>
public class UserType extends TupleType
{
public final String keyspace;
public final ByteBuffer name;
public final List<ByteBuffer> fieldNames;
public final List<AbstractType<?>> fieldTypes;
private final List<ByteBuffer> fieldNames;
public UserType(String keyspace, ByteBuffer name, List<ByteBuffer> fieldNames, List<AbstractType<?>> fieldTypes)
{
super(fieldTypes);
assert fieldNames.size() == fieldTypes.size();
this.keyspace = keyspace;
this.name = name;
this.fieldNames = fieldNames;
this.fieldTypes = fieldTypes;
}
public static UserType getInstance(TypeParser parser) throws ConfigurationException, SyntaxException
@ -65,65 +66,44 @@ public class UserType extends AbstractType<ByteBuffer>
return new UserType(keyspace, name, columnNames, columnTypes);
}
public AbstractType<?> fieldType(int i)
{
return type(i);
}
public List<AbstractType<?>> fieldTypes()
{
return types;
}
public ByteBuffer fieldName(int i)
{
return fieldNames.get(i);
}
public List<ByteBuffer> fieldNames()
{
return fieldNames;
}
public String getNameAsString()
{
return UTF8Type.instance.compose(name);
}
public int compare(ByteBuffer o1, ByteBuffer o2)
{
if (!o1.hasRemaining() || !o2.hasRemaining())
return o1.hasRemaining() ? 1 : o2.hasRemaining() ? -1 : 0;
ByteBuffer bb1 = o1.duplicate();
ByteBuffer bb2 = o2.duplicate();
int i = 0;
while (bb1.remaining() > 0 && bb2.remaining() > 0)
{
AbstractType<?> comparator = fieldTypes.get(i);
int size1 = bb1.getInt();
int size2 = bb2.getInt();
// Handle nulls
if (size1 < 0)
{
if (size2 < 0)
continue;
return -1;
}
if (size2 < 0)
return 1;
ByteBuffer value1 = ByteBufferUtil.readBytes(bb1, size1);
ByteBuffer value2 = ByteBufferUtil.readBytes(bb2, size2);
int cmp = comparator.compare(value1, value2);
if (cmp != 0)
return cmp;
++i;
}
if (bb1.remaining() == 0)
return bb2.remaining() == 0 ? 0 : -1;
// bb1.remaining() > 0 && bb2.remaining() == 0
return 1;
}
// 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
{
ByteBuffer input = bytes.duplicate();
for (int i = 0; i < fieldTypes.size(); i++)
for (int i = 0; i < size(); i++)
{
// we allow the input to have less fields than declared so as to support field addition.
if (!input.hasRemaining())
return;
if (input.remaining() < 4)
throw new MarshalException(String.format("Not enough bytes to read size of %dth field %s", i, fieldNames.get(i)));
throw new MarshalException(String.format("Not enough bytes to read size of %dth field %s", i, fieldName(i)));
int size = input.getInt();
@ -132,10 +112,10 @@ public class UserType extends AbstractType<ByteBuffer>
continue;
if (input.remaining() < size)
throw new MarshalException(String.format("Not enough bytes to read %dth field %s", i, fieldNames.get(i)));
throw new MarshalException(String.format("Not enough bytes to read %dth field %s", i, fieldName(i)));
ByteBuffer field = ByteBufferUtil.readBytes(input, size);
fieldTypes.get(i).validate(field);
types.get(i).validate(field);
}
// We're allowed to get less fields than declared, but not more
@ -143,112 +123,20 @@ public class UserType extends AbstractType<ByteBuffer>
throw new MarshalException("Invalid remaining data after end of UDT value");
}
/**
* Split a UDT value into its fields values.
*/
public ByteBuffer[] split(ByteBuffer value)
@Override
public int hashCode()
{
ByteBuffer[] fields = new ByteBuffer[fieldTypes.size()];
ByteBuffer input = value.duplicate();
for (int i = 0; i < fieldTypes.size(); i++)
{
if (!input.hasRemaining())
return Arrays.copyOfRange(fields, 0, i);
int size = input.getInt();
fields[i] = size < 0 ? null : ByteBufferUtil.readBytes(input, size);
}
return fields;
}
public static ByteBuffer buildValue(ByteBuffer[] fields)
{
int totalLength = 0;
for (ByteBuffer field : fields)
totalLength += 4 + (field == null ? 0 : field.remaining());
ByteBuffer result = ByteBuffer.allocate(totalLength);
for (ByteBuffer field : fields)
{
if (field == null)
{
result.putInt(-1);
}
else
{
result.putInt(field.remaining());
result.put(field.duplicate());
}
}
result.rewind();
return result;
return Objects.hashCode(keyspace, name, fieldNames, types);
}
@Override
public String getString(ByteBuffer value)
{
StringBuilder sb = new StringBuilder();
ByteBuffer input = value.duplicate();
for (int i = 0; i < fieldTypes.size(); i++)
{
if (!input.hasRemaining())
return sb.toString();
if (i > 0)
sb.append(":");
AbstractType<?> type = fieldTypes.get(i);
int size = input.getInt();
if (size < 0)
{
sb.append("@");
continue;
}
ByteBuffer field = ByteBufferUtil.readBytes(input, size);
// We use ':' as delimiter, and @ to represent null, so escape them in the generated string
sb.append(type.getString(field).replaceAll(":", "\\\\:").replaceAll("@", "\\\\@"));
}
return sb.toString();
}
public ByteBuffer fromString(String source)
{
// Split the input on non-escaped ':' characters
List<String> fieldStrings = AbstractCompositeType.split(source);
ByteBuffer[] fields = new ByteBuffer[fieldStrings.size()];
for (int i = 0; i < fieldStrings.size(); i++)
{
String fieldString = fieldStrings.get(i);
// We use @ to represent nulls
if (fieldString.equals("@"))
continue;
AbstractType<?> type = fieldTypes.get(i);
fields[i] = type.fromString(fieldString.replaceAll("\\\\:", ":").replaceAll("\\\\@", "@"));
}
return buildValue(fields);
}
public TypeSerializer<ByteBuffer> getSerializer()
{
return BytesSerializer.instance;
}
@Override
public final int hashCode()
{
return Objects.hashCode(keyspace, name, fieldNames, fieldTypes);
}
@Override
public final boolean equals(Object o)
public boolean equals(Object o)
{
if(!(o instanceof UserType))
return false;
UserType that = (UserType)o;
return keyspace.equals(that.keyspace) && name.equals(that.name) && fieldNames.equals(that.fieldNames) && fieldTypes.equals(that.fieldTypes);
return keyspace.equals(that.keyspace) && name.equals(that.name) && fieldNames.equals(that.fieldNames) && types.equals(that.types);
}
@Override
@ -260,6 +148,6 @@ public class UserType extends AbstractType<ByteBuffer>
@Override
public String toString()
{
return getClass().getName() + TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, fieldTypes);
return getClass().getName() + TypeParser.stringifyUserTypeParameters(keyspace, name, fieldNames, types);
}
}

View File

@ -52,7 +52,8 @@ public enum DataType implements OptionCodec.Codecable<DataType>
LIST (32, null),
MAP (33, null),
SET (34, null),
UDT (48, null);
UDT (48, null),
TUPLE (49, null);
public static final OptionCodec<DataType> codec = new OptionCodec<DataType>(DataType.class);
@ -107,6 +108,12 @@ public enum DataType implements OptionCodec.Codecable<DataType>
fieldTypes.add(DataType.toType(codec.decodeOne(cb, version)));
}
return new UserType(ks, name, fieldNames, fieldTypes);
case TUPLE:
n = cb.readUnsignedShort();
List<AbstractType<?>> types = new ArrayList<>(n);
for (int i = 0; i < n; i++)
types.add(DataType.toType(codec.decodeOne(cb, version)));
return new TupleType(types);
default:
return null;
}
@ -135,13 +142,19 @@ public enum DataType implements OptionCodec.Codecable<DataType>
UserType udt = (UserType)value;
CBUtil.writeString(udt.keyspace, cb);
CBUtil.writeString(UTF8Type.instance.compose(udt.name), cb);
cb.writeShort(udt.fieldNames.size());
for (int i = 0; i < udt.fieldNames.size(); i++)
cb.writeShort(udt.size());
for (int i = 0; i < udt.size(); i++)
{
CBUtil.writeString(UTF8Type.instance.compose(udt.fieldNames.get(i)), cb);
codec.writeOne(DataType.fromType(udt.fieldTypes.get(i), version), cb, version);
CBUtil.writeString(UTF8Type.instance.compose(udt.fieldName(i)), cb);
codec.writeOne(DataType.fromType(udt.fieldType(i), version), cb, version);
}
break;
case TUPLE:
TupleType tt = (TupleType)value;
cb.writeShort(tt.size());
for (int i = 0; i < tt.size(); i++)
codec.writeOne(DataType.fromType(tt.type(i), version), cb, version);
break;
}
}
@ -166,12 +179,18 @@ public enum DataType implements OptionCodec.Codecable<DataType>
size += CBUtil.sizeOfString(udt.keyspace);
size += CBUtil.sizeOfString(UTF8Type.instance.compose(udt.name));
size += 2;
for (int i = 0; i < udt.fieldNames.size(); i++)
for (int i = 0; i < udt.size(); i++)
{
size += CBUtil.sizeOfString(UTF8Type.instance.compose(udt.fieldNames.get(i)));
size += codec.oneSerializedSize(DataType.fromType(udt.fieldTypes.get(i), version), version);
size += CBUtil.sizeOfString(UTF8Type.instance.compose(udt.fieldName(i)));
size += codec.oneSerializedSize(DataType.fromType(udt.fieldType(i), version), version);
}
return size;
case TUPLE:
TupleType tt = (TupleType)value;
size = 2;
for (int i = 0; i < tt.size(); i++)
size += codec.oneSerializedSize(DataType.fromType(tt.type(i), version), version);
return size;
default:
return 0;
}
@ -211,6 +230,9 @@ public enum DataType implements OptionCodec.Codecable<DataType>
if (type instanceof UserType && version >= 3)
return Pair.<DataType, Object>create(UDT, type);
if (type instanceof TupleType && version >= 3)
return Pair.<DataType, Object>create(TUPLE, type);
return Pair.<DataType, Object>create(CUSTOM, type.toString());
}
else
@ -236,6 +258,8 @@ public enum DataType implements OptionCodec.Codecable<DataType>
return MapType.getInstance(l.get(0), l.get(1));
case UDT:
return (AbstractType)entry.right;
case TUPLE:
return (AbstractType)entry.right;
default:
return entry.left.type;
}

View File

@ -30,7 +30,7 @@
<maxFileSize>20MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-5level [%thread] %date{ISO8601} %caller{1} - %msg%n</pattern>
<pattern>%-5level [%thread] %date{ISO8601} %msg%n</pattern>
</encoder>
</appender>
@ -52,7 +52,7 @@
</filter>
</appender>
<root level="DEBUG">
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDERR" />
<appender-ref ref="STDOUT" />

View File

@ -51,6 +51,16 @@ public class SchemaLoader
@BeforeClass
public static void loadSchema() throws ConfigurationException
{
prepareServer();
// if you're messing with low-level sstable stuff, it can be useful to inject the schema directly
// Schema.instance.load(schemaDefinition());
for (KSMetaData ksm : schemaDefinition())
MigrationManager.announceNewKeyspace(ksm);
}
public static void prepareServer()
{
// Cleanup first
cleanupAndLeaveDirs();
@ -69,10 +79,6 @@ public class SchemaLoader
// Migrations aren't happy if gossiper is not started. Even if we don't use migrations though,
// some tests now expect us to start gossip for them.
startGossiper();
// if you're messing with low-level sstable stuff, it can be useful to inject the schema directly
// Schema.instance.load(schemaDefinition());
for (KSMetaData ksm : schemaDefinition())
MigrationManager.announceNewKeyspace(ksm);
}
public static void startGossiper()

View File

@ -0,0 +1,472 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.common.base.Objects;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.utils.ByteBufferUtil;
/**
* Base class for CQL tests.
*/
public abstract class CQLTester
{
protected static final Logger logger = LoggerFactory.getLogger(CQLTester.class);
private static final String KEYSPACE = "cql_test_keyspace";
private static final boolean USE_PREPARED_VALUES = Boolean.valueOf(System.getProperty("cassandra.test.use_prepared", "true"));
private static final AtomicInteger seqNumber = new AtomicInteger();
private String currentTable;
@BeforeClass
public static void setUpClass() throws Throwable
{
// This start gossiper for the sake of schema migrations. We might be able to get rid of that with some work.
SchemaLoader.prepareServer();
schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", KEYSPACE));
}
@AfterClass
public static void tearDownClass()
{
SchemaLoader.stopGossiper();
}
protected void createTable(String query)
{
currentTable = "table_" + seqNumber.getAndIncrement();
String fullQuery = String.format(query, KEYSPACE + "." + currentTable);
logger.info(fullQuery);
schemaChange(fullQuery);
}
private static void schemaChange(String query)
{
try
{
// executeOnceInternal don't work for schema changes
QueryProcessor.process(query, ConsistencyLevel.ONE);
}
catch (Exception e)
{
throw new RuntimeException("Error setting schema for test (query was: " + query + ")", e);
}
}
protected UntypedResultSet execute(String query, Object... values) throws Throwable
{
if (currentTable == null)
throw new RuntimeException("You must create a table first with createTable");
try
{
query = String.format(query, KEYSPACE + "." + currentTable);
UntypedResultSet rs;
if (USE_PREPARED_VALUES)
{
logger.info("Executing: {} with values {}", query, formatAllValues(values));
rs = QueryProcessor.executeOnceInternal(query, transformValues(values));
}
else
{
query = replaceValues(query, values);
logger.info("Executing: {}", query);
rs = QueryProcessor.executeOnceInternal(query);
}
if (rs != null)
logger.info("Got {} rows", rs.size());
return rs;
}
catch (RuntimeException e)
{
Throwable cause = e.getCause() != null ? e.getCause() : e;
logger.info("Got error: {}", cause.getMessage() == null ? cause.toString() : cause.getMessage());
throw cause;
}
}
protected void assertRows(UntypedResultSet result, Object[]... rows)
{
if (result == null)
{
if (rows.length > 0)
Assert.fail(String.format("No rows returned by query but %d expected", rows.length));
return;
}
List<ColumnSpecification> meta = result.metadata();
Iterator<UntypedResultSet.Row> iter = result.iterator();
int i = 0;
while (iter.hasNext() && i < rows.length)
{
Object[] expected = rows[i++];
UntypedResultSet.Row actual = iter.next();
Assert.assertEquals(String.format("Invalid number of (expected) values provided for row %d", i), meta.size(), expected.length);
for (int j = 0; j < meta.size(); j++)
{
ColumnSpecification column = meta.get(j);
Object expectedValue = expected[j];
ByteBuffer expectedByteValue = makeByteBuffer(expected[j], (AbstractType)column.type);
ByteBuffer actualValue = actual.getBytes(column.name.toString());
if (!Objects.equal(expectedByteValue, actualValue))
Assert.fail(String.format("Invalid value for row %d column %d (%s), expected <%s> but got <%s>",
i, j, column.name, formatValue(expectedByteValue, column.type), formatValue(actualValue, column.type)));
}
}
if (iter.hasNext())
{
while (iter.hasNext())
{
iter.next();
i++;
}
Assert.fail(String.format("Got less rows than expected. Expected %d but got %d.", rows.length, i));
}
Assert.assertTrue(String.format("Got more rows than expected. Expected %d but got %d", rows.length, i), i == rows.length);
}
protected void assertAllRows(Object[]... rows) throws Throwable
{
assertRows(execute("SELECT * FROM %s"), rows);
}
protected Object[] row(Object... expected)
{
return expected;
}
protected void assertEmpty(UntypedResultSet result) throws Throwable
{
if (result != null && result.size() != 0)
throw new InvalidRequestException(String.format("Expected empty result but got %d rows", result.size()));
}
protected void assertInvalid(String query, Object... values) throws Throwable
{
try
{
execute(query, values);
Assert.fail("Query should be invalid but no error was thrown. Query is: " + query);
}
catch (SyntaxException | InvalidRequestException e)
{
// This is what we expect
}
}
private static String replaceValues(String query, Object[] values)
{
StringBuilder sb = new StringBuilder();
int last = 0;
int i = 0;
int idx;
while ((idx = query.indexOf('?', last)) > 0)
{
if (i >= values.length)
throw new IllegalArgumentException(String.format("Not enough values provided. The query has at least %d variables but only %d values provided", i, values.length));
sb.append(query.substring(last, idx));
Object value = values[i++];
// When we have a .. IN ? .., we use a list for the value because that's what's expected when the value is serialized.
// When we format as string however, we need to special case to use parenthesis. Hackish but convenient.
if (idx >= 3 && value instanceof List && query.substring(idx - 3, idx).equalsIgnoreCase("IN "))
{
List l = (List)value;
sb.append("(");
for (int j = 0; j < l.size(); j++)
{
if (j > 0)
sb.append(", ");
sb.append(formatForCQL(l.get(j)));
}
sb.append(")");
}
else
{
sb.append(formatForCQL(value));
}
last = idx + 1;
}
sb.append(query.substring(last));
return sb.toString();
}
// We're rellly only returning ByteBuffers but this make the type system happy
private static Object[] transformValues(Object[] values)
{
// We could partly rely on QueryProcessor.executeOnceInternal doing type conversion for us, but
// it would complain with ClassCastException if we pass say a string where an int is excepted (since
// it bases conversion on what the value should be, not what it is). For testing, we sometimes
// want to pass value of the wrong type and assert that this properly raise an InvalidRequestException
// and executeOnceInternal goes into way. So instead, we pre-convert everything to bytes here base
// on the value.
// Besides, we need to handle things like TupleValue that executeOnceInternal don't know about.
Object[] buffers = new ByteBuffer[values.length];
for (int i = 0; i < values.length; i++)
{
Object value = values[i];
if (value == null)
{
buffers[i] = null;
continue;
}
buffers[i] = typeFor(value).decompose(serializeTuples(value));
}
return buffers;
}
private static Object serializeTuples(Object value)
{
if (value instanceof TupleValue)
{
return ((TupleValue)value).toByteBuffer();
}
// We need to reach inside collections for TupleValue and transform them to ByteBuffer
// since otherwise the decompose method of the collection AbstractType won't know what
// to do with them
if (value instanceof List)
{
List l = (List)value;
List n = new ArrayList(l.size());
for (Object o : l)
n.add(serializeTuples(o));
return n;
}
if (value instanceof Set)
{
Set s = (Set)value;
Set n = new LinkedHashSet(s.size());
for (Object o : s)
n.add(serializeTuples(o));
return n;
}
if (value instanceof Map)
{
Map m = (Map)value;
Map n = new LinkedHashMap(m.size());
for (Object entry : m.entrySet())
n.put(serializeTuples(((Map.Entry)entry).getKey()), serializeTuples(((Map.Entry)entry).getValue()));
return n;
}
return value;
}
private static String formatAllValues(Object[] values)
{
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < values.length; i++)
{
if (i > 0)
sb.append(", ");
sb.append(formatForCQL(values[i]));
}
sb.append("]");
return sb.toString();
}
private static String formatForCQL(Object value)
{
if (value == null)
return "null";
if (value instanceof TupleValue)
return ((TupleValue)value).toCQLString();
// We need to reach inside collections for TupleValue. Besides, for some reason the format
// of collection that CollectionType.getString gives us is not at all 'CQL compatible'
if (value instanceof Collection)
{
StringBuilder sb = new StringBuilder();
if (value instanceof List)
{
List l = (List)value;
sb.append("[");
for (int i = 0; i < l.size(); i++)
{
if (i > 0)
sb.append(", ");
sb.append(formatForCQL(l.get(i)));
}
sb.append("[");
}
else if (value instanceof Set)
{
Set s = (Set)value;
sb.append("{");
Iterator iter = s.iterator();
while (iter.hasNext())
{
sb.append(formatForCQL(iter.next()));
if (iter.hasNext())
sb.append(", ");
}
sb.append("}");
}
else
{
Map m = (Map)value;
sb.append("{");
Iterator iter = m.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry entry = (Map.Entry)iter.next();
sb.append(formatForCQL(entry.getKey())).append(": ").append(formatForCQL(entry.getValue()));
if (iter.hasNext())
sb.append(", ");
}
sb.append("}");
}
return sb.toString();
}
AbstractType type = typeFor(value);
String s = type.getString(type.decompose(value));
if (type instanceof UTF8Type)
return String.format("'%s'", s.replaceAll("'", "''"));
if (type instanceof BytesType)
return "0x" + s;
return s;
}
private static ByteBuffer makeByteBuffer(Object value, AbstractType type)
{
if (value == null)
return null;
if (value instanceof TupleValue)
return ((TupleValue)value).toByteBuffer();
if (value instanceof ByteBuffer)
return (ByteBuffer)value;
return type.decompose(value);
}
private static String formatValue(ByteBuffer bb, AbstractType<?> type)
{
return bb == null ? "null" : type.getString(bb);
}
protected Object tuple(Object...values)
{
return new TupleValue(values);
}
protected Object list(Object...values)
{
return Arrays.asList(values);
}
// Attempt to find an AbstracType from a value (for serialization/printing sake).
// Will work as long as we use types we know of, which is good enough for testing
private static AbstractType typeFor(Object value)
{
if (value instanceof ByteBuffer || value instanceof TupleValue || value == null)
return BytesType.instance;
if (value instanceof Integer)
return Int32Type.instance;
if (value instanceof Long)
return LongType.instance;
if (value instanceof Float)
return FloatType.instance;
if (value instanceof Double)
return DoubleType.instance;
if (value instanceof String)
return UTF8Type.instance;
if (value instanceof List)
{
List l = (List)value;
AbstractType elt = l.isEmpty() ? BytesType.instance : typeFor(l.get(0));
return ListType.getInstance(elt);
}
throw new IllegalArgumentException("Unsupported value type (value is " + value + ")");
}
private static class TupleValue
{
private final Object[] values;
TupleValue(Object[] values)
{
this.values = values;
}
public ByteBuffer toByteBuffer()
{
ByteBuffer[] bbs = new ByteBuffer[values.length];
for (int i = 0; i < values.length; i++)
bbs[i] = makeByteBuffer(values[i], typeFor(values[i]));
return TupleType.buildValue(bbs);
}
public String toCQLString()
{
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i = 0; i < values.length; i++)
{
if (i > 0)
sb.append(", ");
sb.append(formatForCQL(values[i]));
}
sb.append(")");
return sb.toString();
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.cql3;
import org.junit.Test;
public class TupleTypeTest extends CQLTester
{
@Test
public void testTuplePutAndGet() throws Throwable
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, t tuple<int, text, double>)");
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 0, tuple(3, "foo", 3.4));
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 1, tuple(8, "bar", 0.2));
assertAllRows(
row(0, tuple(3, "foo", 3.4)),
row(1, tuple(8, "bar", 0.2))
);
// nulls
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 2, tuple(5, null, 3.4));
assertRows(execute("SELECT * FROM %s WHERE k=?", 2),
row(2, tuple(5, null, 3.4))
);
// incomplete tuple
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 3, tuple(5, "bar"));
assertRows(execute("SELECT * FROM %s WHERE k=?", 3),
row(3, tuple(5, "bar"))
);
}
@Test
public void testNestedTuple() throws Throwable
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, t tuple<int, tuple<text, double>>)");
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 0, tuple(3, tuple("foo", 3.4)));
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 1, tuple(8, tuple("bar", 0.2)));
assertAllRows(
row(0, tuple(3, tuple("foo", 3.4))),
row(1, tuple(8, tuple("bar", 0.2)))
);
}
@Test
public void testTupleInPartitionKey() throws Throwable
{
createTable("CREATE TABLE %s (t tuple<int, text> PRIMARY KEY)");
execute("INSERT INTO %s (t) VALUES (?)", tuple(3, "foo"));
assertAllRows(row(tuple(3, "foo")));
}
@Test
public void testTupleInClusteringKey() throws Throwable
{
createTable("CREATE TABLE %s (k int, t tuple<int, text>, PRIMARY KEY (k, t))");
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 0, tuple(5, "bar"));
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 0, tuple(3, "foo"));
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 0, tuple(6, "bar"));
execute("INSERT INTO %s (k, t) VALUES (?, ?)", 0, tuple(5, "foo"));
assertAllRows(
row(0, tuple(3, "foo")),
row(0, tuple(5, "bar")),
row(0, tuple(5, "foo")),
row(0, tuple(6, "bar"))
);
}
@Test
public void testInvalidQueries() throws Throwable
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, t tuple<int, text, double>)");
assertInvalid("INSERT INTO %s (k, t) VALUES (0, ())");
assertInvalid("INSERT INTO %s (k, t) VALUES (0, (2, 'foo', 3.1, 'bar'))");
}
}