cassandra-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From slebre...@apache.org
Subject [2/3] git commit: Tuple types
Date Thu, 29 May 2014 09:37:09 GMT
Tuple types

patch by slebresne; reviewed by thobbs for CASSANDRA-7248


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/0932ed67
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/0932ed67
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/0932ed67

Branch: refs/heads/trunk
Commit: 0932ed670c66ca2f8c5dc1450b85590738b773c9
Parents: 01c5e34
Author: Sylvain Lebresne <sylvain@datastax.com>
Authored: Fri May 23 09:53:07 2014 +0200
Committer: Sylvain Lebresne <sylvain@datastax.com>
Committed: Thu May 29 11:33:11 2014 +0200

----------------------------------------------------------------------
 CHANGES.txt                                     |    1 +
 build.xml                                       |   41 +-
 doc/native_protocol_v3.spec                     |   26 +-
 .../org/apache/cassandra/config/UTMetaData.java |   10 +-
 .../org/apache/cassandra/cql3/CQL3Type.java     |  106 ++
 src/java/org/apache/cassandra/cql3/Cql.g        |   44 +-
 src/java/org/apache/cassandra/cql3/Tuples.java  |  103 +-
 .../apache/cassandra/cql3/UntypedResultSet.java |   18 +
 .../org/apache/cassandra/cql3/UserTypes.java    |   14 +-
 .../cql3/statements/AlterTypeStatement.java     |   26 +-
 .../cql3/statements/CreateTypeStatement.java    |    8 +-
 .../cql3/statements/DropTypeStatement.java      |    2 +-
 .../cql3/statements/SelectStatement.java        |   34 +-
 .../cassandra/cql3/statements/Selection.java    |   10 +-
 .../apache/cassandra/db/marshal/TupleType.java  |   56 +-
 .../apache/cassandra/db/marshal/UserType.java   |  174 +--
 .../apache/cassandra/transport/DataType.java    |   40 +-
 test/conf/logback-test.xml                      |    4 +-
 .../unit/org/apache/cassandra/SchemaLoader.java |   14 +-
 .../org/apache/cassandra/cql3/CQLTester.java    |  472 ++++++
 .../cassandra/cql3/MultiColumnRelationTest.java | 1434 +++++-------------
 .../apache/cassandra/cql3/TupleTypeTest.java    |   97 ++
 22 files changed, 1445 insertions(+), 1289 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index fe8242b..2cb2f5d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -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)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/build.xml
----------------------------------------------------------------------
diff --git a/build.xml b/build.xml
index 680efc1..5740577 100644
--- a/build.xml
+++ b/build.xml
@@ -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">

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/doc/native_protocol_v3.spec
----------------------------------------------------------------------
diff --git a/doc/native_protocol_v3.spec b/doc/native_protocol_v3.spec
index 6719838..400868c 100644
--- a/doc/native_protocol_v3.spec
+++ b/doc/native_protocol_v3.spec
@@ -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.

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/config/UTMetaData.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/config/UTMetaData.java b/src/java/org/apache/cassandra/config/UTMetaData.java
index 178e653..ee653a8 100644
--- a/src/java/org/apache/cassandra/config/UTMetaData.java
+++ b/src/java/org/apache/cassandra/config/UTMetaData.java
@@ -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;
     }
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/CQL3Type.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/CQL3Type.java b/src/java/org/apache/cassandra/cql3/CQL3Type.java
index f07bc19..a26a903 100644
--- a/src/java/org/apache/cassandra/cql3/CQL3Type.java
+++ b/src/java/org/apache/cassandra/cql3/CQL3Type.java
@@ -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();
+            }
+        }
     }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/Cql.g
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g
index 57b61a5..f3681c4 100644
--- a/src/java/org/apache/cassandra/cql3/Cql.g
+++ b/src/java/org/apache/cassandra/cql3/Cql.g
@@ -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;

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/Tuples.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/Tuples.java b/src/java/org/apache/cassandra/cql3/Tuples.java
index 384633d..e1ce551 100644
--- a/src/java/org/apache/cassandra/cql3/Tuples.java
+++ b/src/java/org/apache/cassandra/cql3/Tuples.java
@@ -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);
         }
 
-        public Value bind(QueryOptions options) throws InvalidRequestException
+        private ByteBuffer[] bindInternal(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");
+            // 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 new Value(buffers);
+            return buffers;
+        }
+
+        public Value bind(QueryOptions options) throws InvalidRequestException
+        {
+            return new Value(bindInternal(options));
+        }
+
+        @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();
     }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
index 7e0f15a..42d0cb8 100644
--- a/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
+++ b/src/java/org/apache/cassandra/cql3/UntypedResultSet.java
@@ -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

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/UserTypes.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/UserTypes.java b/src/java/org/apache/cassandra/cql3/UserTypes.java
index ecffe31..c5469d2 100644
--- a/src/java/org/apache/cassandra/cql3/UserTypes.java
+++ b/src/java/org/apache/cassandra/cql3/UserTypes.java
@@ -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;
         }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
index eac936f..1996457 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterTypeStatement.java
@@ -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())
             {

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
index cd3e3e5..cc5447e 100644
--- a/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/CreateTypeStatement.java
@@ -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)));

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/statements/DropTypeStatement.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/DropTypeStatement.java b/src/java/org/apache/cassandra/cql3/statements/DropTypeStatement.java
index 10fc366..1e1ded5 100644
--- a/src/java/org/apache/cassandra/cql3/statements/DropTypeStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/DropTypeStatement.java
@@ -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;
         }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index 4a84b7b..9d63389 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -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

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/cql3/statements/Selection.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/Selection.java b/src/java/org/apache/cassandra/cql3/statements/Selection.java
index 4990e11..f4e0885 100644
--- a/src/java/org/apache/cassandra/cql3/statements/Selection.java
+++ b/src/java/org/apache/cassandra/cql3/statements/Selection.java
@@ -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)));
         }
     }
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/db/marshal/TupleType.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/db/marshal/TupleType.java b/src/java/org/apache/cassandra/db/marshal/TupleType.java
index 74211c8..e754b51 100644
--- a/src/java/org/apache/cassandra/db/marshal/TupleType.java
+++ b/src/java/org/apache/cassandra/db/marshal/TupleType.java
@@ -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()
@@ -271,9 +288,14 @@ public class TupleType extends AbstractType<ByteBuffer>
     }
 
     @Override
+    public CQL3Type asCQL3Type()
+    {
+        return CQL3Type.Tuple.create(this);
+    }
+
+    @Override
     public String toString()
     {
         return getClass().getName() + TypeParser.stringifyTypeParameters(types);
     }
 }
-

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/db/marshal/UserType.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java
index 6656fd6..44c208f 100644
--- a/src/java/org/apache/cassandra/db/marshal/UserType.java
+++ b/src/java/org/apache/cassandra/db/marshal/UserType.java
@@ -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 String getNameAsString()
+    public AbstractType<?> fieldType(int i)
     {
-        return UTF8Type.instance.compose(name);
+        return type(i);
     }
 
-    public int compare(ByteBuffer o1, ByteBuffer o2)
+    public List<AbstractType<?>> fieldTypes()
     {
-        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;
+        return types;
+    }
 
-            ++i;
-        }
+    public ByteBuffer fieldName(int i)
+    {
+        return fieldNames.get(i);
+    }
 
-        if (bb1.remaining() == 0)
-            return bb2.remaining() == 0 ? 0 : -1;
+    public List<ByteBuffer> fieldNames()
+    {
+        return fieldNames;
+    }
 
-        // bb1.remaining() > 0 && bb2.remaining() == 0
-        return 1;
+    public String getNameAsString()
+    {
+        return UTF8Type.instance.compose(name);
     }
 
+    // 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)
-    {
-        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;
-    }
-
-    @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()
+    public int hashCode()
     {
-        return Objects.hashCode(keyspace, name, fieldNames, fieldTypes);
+        return Objects.hashCode(keyspace, name, fieldNames, types);
     }
 
     @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);
     }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/src/java/org/apache/cassandra/transport/DataType.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/transport/DataType.java b/src/java/org/apache/cassandra/transport/DataType.java
index 2410378..0710645 100644
--- a/src/java/org/apache/cassandra/transport/DataType.java
+++ b/src/java/org/apache/cassandra/transport/DataType.java
@@ -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;
             }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/test/conf/logback-test.xml
----------------------------------------------------------------------
diff --git a/test/conf/logback-test.xml b/test/conf/logback-test.xml
index a7aa34c..535e4fe 100644
--- a/test/conf/logback-test.xml
+++ b/test/conf/logback-test.xml
@@ -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" />

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/test/unit/org/apache/cassandra/SchemaLoader.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/SchemaLoader.java b/test/unit/org/apache/cassandra/SchemaLoader.java
index cbff31e..7f637a4 100644
--- a/test/unit/org/apache/cassandra/SchemaLoader.java
+++ b/test/unit/org/apache/cassandra/SchemaLoader.java
@@ -52,6 +52,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()

http://git-wip-us.apache.org/repos/asf/cassandra/blob/0932ed67/test/unit/org/apache/cassandra/cql3/CQLTester.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java
new file mode 100644
index 0000000..06119ed
--- /dev/null
+++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java
@@ -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();
+        }
+    }
+}


Mime
View raw message